如何設定 CORS

Sonui發表於2024-11-20

0x00 引言

CORS 全稱是跨域資源共享(Cross-Origin Resource Sharing),是一種安全機制,用於限制哪些源(origin)可以訪問伺服器上的資源,這裡探討下該如何正確的設定 CORS。

TLDR

關鍵配置要點:

  • Access-Control-Allow-Origin: 禁止使用 *,應該設定為具體的白名單域名
  • Access-Control-Allow-Methods: 明確指定允許的 HTTP 方法
  • Access-Control-Allow-Headers: 明確指定允許的請求頭
  • Access-Control-Allow-Credentials: 如需攜帶認證資訊,必須設定為 true

0x01 跨域觸發條件

flowchart A[瀏覽器] B[請求URL] C[是否同源] D[獲取同源策略] E[允許訪問] F[觸發CORS檢查] G[跨域錯誤] H[是否簡單請求] I[Preflight] J[正式請求] A --> |發起請求| B B --> |檢查| C C --> |否| F C --> |是同協議+域名+埠| E D --> |檢查請求體| H H --> |是| E H --> |否| I I --> |允許| E I --> |不允許| G

注:簡單請求的定義:

  1. 請求方法為 GET、POST 或 HEAD
  2. 請求頭只包含安全的欄位(Accept、Accept-Language、Content-Language、Content-Type等)
  3. Content-Type 只限於:application/x-www-form-urlencoded、multipart/form-data、text/plain

這裡只選取了部分內容,更多內容請參考 MDN 文件

0x02 CORS 規範

當為非同源非簡單請求時會觸發 preflight 檢查,瀏覽器會先發出一個 OPTIONS 請求,用於檢查伺服器是否支援該請求,其請求頭資訊和正式請求一致,但不會攜帶Body。

🚨 重要提示: 為什麼特別強調非簡單請求?當瀏覽器認為這個請求為簡單請求時,不會透過兩次請求進行檢查,也就是沒有 OPTIONS preflight 請求,是直接傳送正式請求,然後根據正式請求的返回頭判定是否跨域決定指令碼是否可以讀取返回內容。

因此也帶來一個問題,雖然前端讀取不到伺服器返回的資訊,但請求是真實發出去伺服器執行了的,如果伺服器沒有正確的跨域處理中介軟體則會導致安全問題。

Example:

OPTIONS /api/v1/user HTTP/1.1
Origin: http://hacker.com
Sec-Fetch-Dest: empty
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: cors

伺服器在收到 OPTIONS 請求後,需要根據配置進行檢查,如果允許則返回以下資訊:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://hacker.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization

0x03 設定建議

Origins

建立一箇中介軟體,在處理器處理之前檢查 OriginMethod,首先判斷 Origin 是否在白名單內,如果在白名單允許範圍內再檢查 Method,如果是 OPTIONS 請求則直接返回 204 No Content,否則進入下一步繼續處理請求。

Methods

可以設定為 *,如果需要限制則設定為白名單。

Headers

可以設定為 *,推薦限制則設定為白名單。注意,在 Authorization 標頭不能被泛化處理,始終需要明確列出。如果伺服器還提供影片服務且為白名單策略時需要新增一個 Range 頭。

Credentials

如果設定為 true,則允許瀏覽器在跨域請求中攜帶 Cookie 資訊,但同時 Access-Control-Allow-Origin 不能設定為 * 必須明確指出具體域名,如果設定為 * 會無效。

Expose-Headers

無所謂,可以不指定,如果瀏覽器需要訪問未在 Access-Control-Allow-Headers 中列出的頭可以透過設定這個頭來解決。

0x04 示例程式碼

const allowedOrigins = ['https://example.com', 'https://api.example.com'];

app.use((req, res, next) => {
  const origin = req.headers.origin;
  if (allowedOrigins.includes(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin);
    res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    res.setHeader('Access-Control-Allow-Credentials', 'true');
  }
  
  if (req.method === 'OPTIONS') {
    return res.sendStatus(204);
  }
  
  next();
});

特別注意:

  1. 預檢請求的快取:可透過 Access-Control-Max-Age 設定預檢請求的快取時間,避免重複傳送
  2. 錯誤處理:當 CORS 檢查失敗時,瀏覽器會在控制檯輸出詳細的錯誤資訊,但實際的網路請求響應會被瀏覽器攔截,JavaScript 無法訪問具體的錯誤資訊

相關文章