這篇整理自 2024 年 11 月在前端讀書會的分享。

起因是工作上遇到一個 CORS 問題,追了一輪來龍去脈;剛好那陣子在讀胡立的《Beyond XSS:探索網頁前端資安宇宙》,就決定從 CORS 出發,順便把瀏覽器的安全模型整理一遍。

從 CORS 看同源政策

CORS 全名是 Cross-Origin Resource Sharing,跨來源資源共享。Cross-origin 是相對於 same-origin(同源)的概念,所以要理解 CORS,得先從同源政策談起。

MDN 對同源政策的定義是:限制「從某個 origin 載入的 document 或 script,能夠如何與另一個 origin 的資源互動」的安全機制。

什麼是 origin

一個 origin 由三個部分組成:

  • scheme(protocol),例如 http、https
  • hostname(domain),例如 example.com
  • port,例如 3000

三個部分都相同,才算同源。path 不影響。

另一個容易混淆的概念是 same site。same site 看的是 scheme 加上 registrable domain,不看 port,也不看子網域:

A B same origin same site 說明
http://example.com:8080 http://example.com ✗ ✓ same site 不看 port
https://blog.example.com https://example.com ✗ ✓ registrable domain 相同
https://alice.github.io https://github.io ✗ ✗ github.io 在 public suffix list 裡
https://a.alice.github.io https://b.alice.github.io ✗ ✓ registrable domain 相同
https://bob.github.io/page1 https://bob.github.io/about ✓ ✓ 不看 path

更完整的說明可以看 Beyond XSS 的〈Same-origin 與 same-site〉。

為什麼會有 CORS 錯誤

同源政策預設會擋下跨來源的資源請求,CORS 則讓伺服器可以宣告「哪些來源可以存取我的資源」,在安全的前提下開放跨來源傳輸資料。

要記得的是,CORS 錯誤只會出現在瀏覽器裡。同樣的請求用 Node.js 或 curl 發送,不會有這個問題。瀏覽器之所以要有同源政策,是因為它同時開著許多網站,又帶著使用者的登入狀態,如果任何網站都能讀取其他網站的回應,資料就會外洩。

哪些請求會受 CORS 限制

  • 用 fetch() 或 XMLHttpRequest 發出的請求
  • 在 CSS @font-face 中跨來源載入的 Web Fonts
  • WebGL textures
  • 用 drawImage() 畫到 canvas 上的圖片或影片畫格
  • 用 CSS shape-outside 從圖片產生的形狀(文繞圖效果)

後面幾項和圖像有關。這些情境需要讀取圖片的像素資料,例如 CSS Shapes 會分析圖片的 alpha channel,不只是把圖片嵌入網頁,所以限制比較嚴格。

反過來說,<img>、<script>、<link> 載入跨來源資源不需要 CORS。這些資源是網頁的一部分,載入後程式碼讀不到內容,也就無法把結果傳到別處,資料外洩的疑慮比較低。

如何設定 CORS

由後端設定 header

CORS 處理的是跨來源的「資源」,而資源在後端伺服器手上,所以應該由伺服器決定哪些網站可以存取。

做法是在 HTTP response 加上 Access-Control-Allow-Origin header,值是要放行的 origin:

Access-Control-Allow-Origin: https://app.example.com

瀏覽器收到 response 後,會依照這個 header 判斷發出請求的網站有沒有資格讀取內容。注意 request 其實已經送出、伺服器也回應了,擋下來的是瀏覽器把 response 交給 JavaScript 這一步。

對後端沒有掌控權時:proxy server

如果要取得的是別人的資源,對方又沒有提供 Access-Control-Allow-Origin,可以考慮架一個 proxy server:

  1. 瀏覽器把請求送到自己的 proxy server
  2. proxy server 轉送請求到目標伺服器
  3. 目標伺服器回應給 proxy server
  4. proxy server 加上 CORS header,再回傳給瀏覽器

瀏覽器透過自己的 proxy server 存取目標伺服器的流程 CORS header 由自己掌控的 proxy 加上

proxy server 和目標伺服器之間的溝通不經過瀏覽器,不受同源政策限制;proxy server 又是自己掌控的,可以自行加上 CORS header,瀏覽器收到時就不會有 CORS 問題。胡立在〈CORS 完全手冊(二)〉有畫圖說明這個流程。

CORS 的眉眉角角

預檢請求

遇到 CORS 錯誤時,request 到底有沒有送出去?答案是:看情況。這和 CORS 的預檢請求(preflight request)機制有關。

瀏覽器會先判斷這個跨來源請求是不是簡單請求(simple request)。簡單請求大致要符合:

  • method 是 GET、HEAD 或 POST
  • 只使用 CORS-safelisted 的 header,例如 Accept、Accept-Language、Content-Type
  • Content-Type 只能是 application/x-www-form-urlencoded、multipart/form-data 或 text/plain

完整條件見 MDN。

不符合的請求,例如送 application/json 的 POST,或是帶了自訂 header 的請求,瀏覽器會先用 OPTIONS method 發一個預檢請求,確認伺服器允許之後,才會送出真正的請求:

OPTIONS /api/data HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type, x-custom-header

預檢請求會帶上 Origin、Access-Control-Request-Method,以及(不一定有)Access-Control-Request-Headers,讓伺服器知道真正的請求會用到哪些 method 和 header。伺服器要回應對應的 Access-Control-Allow-Methods、Access-Control-Allow-Headers,雙方的認知一致,瀏覽器才會放行。

預檢請求的流程:瀏覽器先送 OPTIONS,伺服器同意後才送出 POST 預檢通過,瀏覽器才會送出真正的請求

所以,如果是在預檢階段失敗,真正的請求根本不會送出;如果是簡單請求,請求會送到伺服器,只是瀏覽器不讓前端讀取 response。

其他情境推薦閱讀胡立的〈CORS 完全手冊(三):CORS 詳解〉。

帶憑證的請求不能用 *

如果請求帶有憑證(credentialed request),例如帶著 cookie,以下 header 都不能設成 *:

  • Access-Control-Allow-Origin:要指定一個 origin
  • Access-Control-Allow-Headers:要列出 header 名稱
  • Access-Control-Allow-Methods:要列出 method 名稱
  • Access-Control-Expose-Headers:要列出 header 名稱

另外,Access-Control-Allow-Origin 設成 * 時,response 裡的 Set-Cookie 不會真的寫入 cookie。

其他跨來源 header

CORS 之外,還有幾個名字很像的 header,它們的來由是 2018 年公開的 CPU 漏洞 Meltdown 與 Spectre。

這類攻擊針對 CPU 的 data cache,放到瀏覽器的情境,就是攻擊者有機會讀到同一個 process 裡其他網站的資料。所以這些跨來源 header 的共同目的,是不讓惡意網站和目標網站處在同一個 process。

CORB(Cross-Origin Read Blocking)

CORB 是瀏覽器內建的機制,不是 header。它處理的是不受 CORS 限制的資源載入,阻止不合理的跨來源資源被放進 render process,例如用 <img> 載入 JSON,或用 <script> 載入 HTML。保護的資料類型主要是 HTML、XML 和 JSON。

由於很多網站的 Content-Type 設錯,Chrome 會探測(sniff)內容來判斷實際的檔案類型,再決定要不要套用 CORB。如果確定伺服器給的 Content-Type 都正確,可以加上:

X-Content-Type-Options: nosniff

告訴瀏覽器直接採用給定的類型,不要自己探測,避免誤判。

CORP(Cross-Origin Resource Policy)

CORB 保護 HTML、XML 與 JSON,其他資源,例如用 <img> 載入的圖片,則由 CORP 處理。伺服器可以把 Cross-Origin-Resource-Policy 設成 same-site、same-origin 或 cross-origin,限制資源能不能被跨來源載入。

CORP 出現之後,伺服器第一次有辦法阻止「任何跨來源載入」。這不只是安全考量,也能用來阻止別的網站直接引用自己伺服器上的圖片。

COEP 與 COOP

Cross-Origin-Embedder-Policy(COEP)和 Cross-Origin-Opener-Policy(COOP)兩者搭配,可以讓網頁進入 cross-origin isolated 狀態,這是比 Site Isolation 更嚴格的隔離。細節可以看 Beyond XSS 的相關章節。

參考資料