
從 CORS 淺談瀏覽器安全模型
這篇整理自 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:
- 瀏覽器把請求送到自己的 proxy server
- proxy server 轉送請求到目標伺服器
- 目標伺服器回應給 proxy server
- proxy server 加上 CORS header,再回傳給瀏覽器
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,雙方的認知一致,瀏覽器才會放行。
預檢通過,瀏覽器才會送出真正的請求
所以,如果是在預檢階段失敗,真正的請求根本不會送出;如果是簡單請求,請求會送到伺服器,只是瀏覽器不讓前端讀取 response。
其他情境推薦閱讀胡立的〈CORS 完全手冊(三):CORS 詳解〉。
帶憑證的請求不能用 *
如果請求帶有憑證(credentialed request),例如帶著 cookie,以下 header 都不能設成 *:
Access-Control-Allow-Origin:要指定一個 originAccess-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 的相關章節。