題目與適用情境
頁面執行於 https://app.example.com,API 位於 https://api.example.com。前端需要提交使用者偏好:
await fetch("https://api.example.com/profile/preferences", {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": csrfToken,
},
body: JSON.stringify({ compactMode: true }),
})伺服器目前對 OPTIONS 回傳:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POSTcurl 直接呼叫 POST 可以成功,瀏覽器主控台卻只顯示跨來源錯誤。請說明:
- 為什麼這兩個子網域仍是跨來源請求;
- 瀏覽器為什麼先傳送 OPTIONS,什麼情況下才會繼續傳送 POST;
- 目前的回應標頭有哪些問題,應該如何依最小權限修正;
- CORS、Cookie、驗證、授權和 CSRF 分別負責什麼;
- 如何在開發者工具與自動化測試中證明修正有效。
這道題適用於前端與全端職缺。它考察的不是背一組回應標頭,而是能否從瀏覽器網路階段反推 故障位置,並把「請求是否傳送」「憑證是否攜帶」「指令碼能否讀取回應」「業務是否允許操作」 四件事拆開。
面試官考察重點
首先要準確判斷來源。Origin 由通訊協定、主機和連接埠組成。兩個位址雖然同為 HTTPS,且通常 屬於同一站點,但主機不同,因此是同站跨來源請求。CORS 依來源判斷;Cookie 的 SameSite 依站點判斷,兩者不能混用。
其次要理解預檢條件。application/json 不屬於 CORS-safelisted 請求標頭值, X-CSRF-Token 也不是 safelisted 請求標頭,因此瀏覽器先傳送 OPTIONS,詢問伺服器是否允許 實際方法與請求標頭。預檢失敗時,實際 POST 不會傳送。
第三,要能讀出目前設定的兩個關鍵錯誤:帶憑證的回應不能使用 Access-Control-Allow-Origin: *;預檢也沒有允許 Content-Type 與 X-CSRF-Token。允許方法中出現 POST 還不夠。
最後要守住安全邊界。CORS 決定瀏覽器是否把跨來源回應共享給指令碼,不取代驗證、授權或 CSRF 防護。伺服器不能為了消除主控台錯誤就反射任意 Origin,也不應把所有方法與請求標頭 全部放開。
回答前需要釐清的問題
- 失敗發生在哪個網路階段? Network 面板裡完全沒有請求、只有 OPTIONS、已有 POST 但
JavaScript 讀不到回應,代表不同故障。
- 請求是否必須帶 Cookie? 若使用 Bearer token 或匿名公開資源,憑證規則與允許來源策略會
不同。
- 允許哪些精確的前端 Origin? 需要列出生產、預備和本機開發來源,不能只說「允許公司
網域」。通訊協定與連接埠也必須精確。
- OPTIONS 是否被閘道、驗證中介軟體或重新導向攔截? 預檢通常不帶使用者憑證,不能先
要求它通過登入狀態驗證。
- Cookie 的 Domain、Path、Secure 與 SameSite 是什麼? CORS 通過也不保證瀏覽器會傳送
Cookie。
- 實際回應與錯誤回應是否都帶 CORS 標頭? 若 401、403 或 500 遺漏允許來源標頭,瀏覽器
可能用泛化的 CORS 錯誤遮住真正狀態。
- 是否存在 CDN 或共享快取? 如果回應會隨 Origin 動態變化,應傳送
Vary: Origin,避免
一個來源的回應被錯誤重用給另一個來源。
30 秒回答框架
「我會把問題拆成四道關卡:來源判斷、預檢許可、憑證傳輸和回應共享。兩個子網域主機不同, 所以是跨來源。JSON Content-Type 和 X-CSRF-Token 會觸發 OPTIONS;瀏覽器只有看到精確允許的 Origin、POST 和兩個請求標頭,才會傳送實際請求。這裡使用 credentials: include,所以 Allow-Origin 不能是星號,應該在嚴格允許清單命中後回傳 https://app.example.com,同時回傳 Allow-Credentials: true 和 Vary: Origin。OPTIONS 本身不依賴登入 Cookie;真正的 POST 仍要做驗證、授權與 CSRF 檢查。最後我會在 Network 面板逐個核對 OPTIONS、POST、Cookie、回應標頭與業務狀態,並用允許來源與惡意來源的矩陣 驗證。」
分步深入解答
第一步:先畫出瀏覽器實際執行的狀態機。
預檢請求大致如下:
OPTIONS /profile/preferences HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type,x-csrf-token瀏覽器比較的不是「前後端是不是同一家公司」,而是回應是否明確許可這個 Origin、方法和 請求標頭。可以用下表快速定位:
| Network 證據 | 說明 | 下一步 | |---|---|---| | 沒有 OPTIONS 或 POST | JavaScript 尚未執行、URL 被 CSP 等更早規則攔截 | 先檢查呼叫與主控台 | | 只有失敗的 OPTIONS | 實際 POST 未傳送 | 檢查預檢狀態碼、重新導向和允許標頭 | | OPTIONS 成功,POST 無 Cookie | CORS 許可已通過,憑證鏈路有問題 | 檢查 credentials 與 Cookie 屬性 | | POST 回傳 401/403/500,但指令碼只見 CORS | 錯誤回應缺少有效 CORS 標頭 | 保留真實狀態並補齊允許來源標頭 | | POST 成功且指令碼可讀 | CORS 鏈路通過 | 再驗證業務狀態與安全負例 |
第二步:依請求推導最小允許集合。
對於題目中唯一允許的生產前端,可以回傳:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: Content-Type, X-CSRF-Token
Vary: Origin伺服器應先把 Origin 與精確允許清單比較,命中後才寫回該值。不要把用戶端傳來的 Origin 無條件複製到回應。若還有預備或本機環境,應把每個完整 Origin 分別列入相應環境設定, 不要用字串後綴比對來接受偽造的相似網域。
公開、匿名且不帶憑證的資源有時可以使用 *。這個使用者偏好介面帶 Cookie,屬於敏感操作, 必須回傳具體 Origin。預檢 OPTIONS 不攜帶 Cookie;閘道可以在驗證前處理預檢,但仍要嚴格 檢查 Origin、方法與請求標頭。
第三步:再檢查實際請求與憑證。
預檢成功後,瀏覽器才會傳送 POST。credentials: "include" 允許跨來源請求進入憑證處理, 但 Cookie 是否真正附帶仍受 Domain、Path、Secure、SameSite 與瀏覽器隱私策略約束。
本例兩個 HTTPS 子網域通常是同站跨來源:SameSite 規則可能允許 Cookie,CORS 仍然適用。 不能從「CORS 已通過」推導「Cookie 一定傳送」,也不能從「Cookie 已傳送」推導「指令碼可以 讀取回應」。應在 Network 面板分別檢查 Request Cookies、回應狀態與 CORS 回應標頭。
第四步:把 CORS 與業務安全拆開。
CORS 是瀏覽器的回應共享協定,不是伺服器存取控制。curl 和伺服器端 HTTP 用戶端不會替 瀏覽器執行同源策略,所以 curl 成功只能證明 API 能處理請求,不能證明瀏覽器鏈路正確。
實際 POST 仍需:
- 驗證登入身分;
- 驗證目前使用者是否有權修改目標資源;
- 驗證 CSRF token 或採用同等強度的防護;
- 檢查輸入並維持冪等或重複提交策略;
- 對允許來源的 401、403 和 500 回應同樣回傳正確 CORS 標頭,讓前端看見真實錯誤。
「惡意頁面讀不到回應」不等於「請求沒有副作用」。某些不需要預檢的跨來源表單式請求可能 已經到達伺服器,因此敏感寫入不能只靠 CORS 防 CSRF。
第五步:用正負測試證明邊界。
修正後至少驗證:
| 測試案例 | 預期結果 | |---|---| | 允許來源傳送題目請求 | OPTIONS 204,隨後 POST;指令碼讀到真實回應 | | 允許來源缺少 CSRF token | 依協定設定決定是否預檢;實際寫入必須被業務檢查拒絕 | | 未允許來源傳送相同請求 | 不回傳允許該來源的 CORS 標頭,實際預檢不通過 | | 請求未允許的方法或請求標頭 | 預檢失敗,POST 不傳送,狀態不變 | | Cookie 過期 | POST 回傳真實 401,前端可讀取並進入登入流程 | | 伺服器回傳 500 | 前端看到 500 與結構化錯誤,而非泛化 CORS 提示 | | 連續相同請求命中預檢快取 | 行為仍正確;設定變更時重新驗證冷快取 |
同時核對伺服器日誌:失敗的預檢不應出現對應 POST,也不應產生使用者偏好更新。日誌可以 記錄 Origin、方法、請求標頭集合和拒絕原因,不應記錄 Cookie 或 CSRF token 值。
高品質示範回答
「我先判斷請求所處階段。app.example.com 和 api.example.com 的主機不同,所以即使它們 同站,仍然跨來源。這個 POST 使用 JSON Content-Type 和 X-CSRF-Token,瀏覽器會先發一個 不帶 Cookie 的 OPTIONS,其中宣告 Origin、POST 和兩個非簡單請求標頭。只有預檢回應允許 這三項,瀏覽器才繼續傳送 POST。
目前設定有兩個直接問題。第一,前端設定了 credentials: include,帶憑證回應不能把 Access-Control-Allow-Origin 設為星號;第二,回應沒有 Access-Control-Allow-Headers: Content-Type, X-CSRF-Token。我會在伺服器用精確允許清單 檢查 Origin,命中 https://app.example.com 後回傳這個具體值、Allow-Credentials、POST、 兩個允許請求標頭以及 Vary: Origin。OPTIONS 放在使用者驗證中介軟體之前處理,但未允許的 Origin、方法或請求標頭仍會被拒絕。
預檢通過後,我再檢查 POST 的 Request Cookies。credentials: include 只是必要條件,Cookie 仍受 Domain、Path、Secure、SameSite 和瀏覽器策略約束。真正的 POST 必須繼續做驗證、授權 與 CSRF 檢查,因為 CORS 只控制瀏覽器是否把回應共享給指令碼,不是存取控制。
驗證時我會在 Network 面板確認 OPTIONS 成功後才出現 POST,檢查請求 Cookie、實際狀態與 回應標頭;再用允許來源、未允許來源、未允許方法、過期登入狀態和 500 回應做正負測試。 伺服器日誌還要證明預檢失敗時沒有 POST,也沒有狀態變化。curl 成功只能作為 API 基線, 不能取代瀏覽器 CORS 驗證。」
常見錯誤
- 把同站當成同源 → 子網域不同仍會觸發 CORS → 依通訊協定、主機、連接埠逐項比較。
- 看到 POST 在允許清單就認為預檢會過 → 非簡單請求標頭未被允許 → **同時核對 Origin、
Method 和 Headers。**
- 帶憑證時使用
Allow-Origin:→ 瀏覽器拒絕把回應暴露給指令碼 → *回傳允許清單
命中的具體 Origin。**
- 無條件回顯請求 Origin → 任意網站都可能獲得帶憑證回應的讀取權限 → **先做精確允許
清單檢查。**
- 要求 OPTIONS 先通過使用者登入 → 預檢通常不攜帶憑證 → **在驗證前完成受限的預檢
處理。**
- 用
mode: "no-cors"修正 → 前端拿到的是不可讀的 opaque response → **修正伺服器
CORS 協定。**
- 把 CORS 當作 CSRF 防護 → 請求可能到達伺服器並產生副作用 → **敏感寫入繼續檢查
CSRF 與授權。**
- 只給 2xx 加 CORS 標頭 → 401 或 500 被偽裝成跨來源錯誤 → **允許來源的錯誤回應也
回傳一致 CORS 標頭。**
- 只用 curl 驗證 → curl 不執行瀏覽器同源策略 → 必須用真實瀏覽器網路證據驗收。
追問與應對
為什麼請求有時不發 OPTIONS,卻仍然回報 CORS?
符合 safelisted 方法、請求標頭與 Content-Type 的請求可以直接傳送。瀏覽器仍會檢查實際回應 的 CORS 標頭;不符合共享條件時,JavaScript 讀不到回應。此時伺服器可能已經處理請求,所以 要從 Network 面板確認「請求已傳送但回應未共享」,不能把所有 CORS 錯誤都稱為預檢失敗。
為什麼 Access-Control-Allow-Credentials 不應該出現在預檢請求裡?
它是伺服器的回應標頭,表示瀏覽器可以向指令碼暴露憑證模式下的實際回應。用戶端透過 Fetch 的 credentials mode 表達憑證意圖;預檢請求本身不帶使用者憑證。實際回應也要符合具體允許 來源和 Allow-Credentials 條件。
Vary: Origin 解決什麼問題?
當伺服器依請求 Origin 回傳不同的 Access-Control-Allow-Origin 時,共享快取需要把 Origin 納入快取鍵。否則,一個允許來源的回應可能被重用給另一個來源,造成錯誤放行或錯誤攔截。
CORS 已正確,為什麼 Cookie 仍然沒有傳送?
繼續檢查前端是否設定 credentials: "include",以及 Cookie 的 Domain、Path、Secure、 SameSite、到期時間與瀏覽器隱私限制。CORS 與 Cookie 傳輸是相鄰但獨立的關卡。
如何處理多個合法前端網域?
維護精確允許清單,請求到達時檢查完整 Origin,命中後回寫該 Origin 並傳送 Vary: Origin。 不要回傳多個 Allow-Origin 值,也不要用寬鬆正規表示式、後綴比對或無條件反射。
前端能否自行修正伺服器 CORS?
不能。前端可以減少不必要的非簡單請求標頭,或依協定設定 credentials,但允許跨來源共享的 回應標頭必須由 API、反向代理或閘道正確回傳。生產程式碼不應依賴瀏覽器外掛或關閉安全策略。
應該把預檢快取多久?
根據設定變更頻率、撤銷需求和瀏覽器上限選擇,並在發佈 CORS 變更時驗證冷快取與既有快取 兩條路徑。快取能減少 OPTIONS 流量,但會延長錯誤許可的生效時間,不能用來掩蓋設定問題。