--- title: Web Security 101 - Same Origin Policy and Cross-Origin Resource Sharing (CORS) --- ###### tags: `jubo` `tutorials` `web security` [TOC] :::info :information_source: demo 須知 - 使用的原始碼皆放在 https://github.com/hjcian/web-security-tutorials - 以下 demo 時執行 `make` 都是都在資料夾根路徑下執行 ::: ## Same Origin Policy ### What is "Origin"? ![](https://i.imgur.com/mHrGspl.png) > See also > - https://developer.mozilla.org/en-US/docs/Web/API/Location ### Basic Rules - 所謂的「同源 (Same Origin)」是指兩個網站的「通訊協定 (protocol)」、「主機名稱 (hostname)」與「埠號 (port)」皆相同 - 網頁安全模型,**在原則上,不同源的網站之間不允許通訊**,以保障最基本的網路安全 - 給定 `http://store.company.com/dir/page.html` ,以下各 URLs 與之同源與否: 1. ✅ `http://store.company.com/dir2/other.html` 2. ✅ `http://store.company.com/dir/inner/another.html` 3. 📛 `https://store.company.com/page.html` 4. 📛 `http://store.company.com:81/dir/page.html` 5. 📛 `http://news.company.com/dir/page.html` - 但實際的預設規則是 - 透過 **HTML tag (embedding) 內**發起的 GET 請求,**就算非同源,通常都會被允許** - 透過 **Javascript 程式碼**去發起的請求,都會被限制 - 以下以一份 HTML ([source](https://github.com/hjcian/web-security-tutorials/blob/main/same-origin-policy/index.html)) 示範 tag 會被允許、而 JS code 會被 CORS 阻擋的例子: ```htmlembedded= <html> <head> <!-- ✅ cross-origin embedding is OK --> <link rel="stylesheet" href="https://example.com/embeded.link.GET.is.OK"> </head> <body> <h1> This Same Origin Policy Demo </h1> <br/><br/> F12 打開 Console 觀察哪些存取方式會被 CORS 擋下來 <br/><br/> <!-- ✅ cross-origin embedding is OK --> <script src="https://example.com/embeded.script.GET.is.OK" style="display: none;"></script> <iframe src="https://example.com/embeded.iframe.GET.is.OK" style="display: none;"></iframe> <img src="https://example.com/embeded.img.GET.is.OK" style="display: none;"></img> <!-- ✅ cross-origin writes are OK --> <iframe name="hiddenFrame" style="display: none;"></iframe> <form action="https://example.com/form.POST.is.OK" method="POST" class="form-example" target="hiddenFrame"> <input type="submit" value="Test form"> </form> <!-- 📛 cross-origin reads are NG --> <script> fetch("https://example.com/js.script.GET.will.be.blocked.by.cors").then(function(response) { return response.text(); }).then(function(text) { console.log("should NOT fetch OK", text); }).catch(function(error) { console.log("fetch error", error); }); </script> </body> </html> ``` :::info demo steps - `make run-same-origin-policy-demo` - open localhost:3000 - open **DevTool** and see the console ::: ![](https://i.imgur.com/f9LOH0J.png) > See also > - [簡單弄懂同源政策 (Same Origin Policy) 與跨網域 (CORS) - StarBugs](https://medium.com/starbugs/%E5%BC%84%E6%87%82%E5%90%8C%E6%BA%90%E6%94%BF%E7%AD%96-same-origin-policy-%E8%88%87%E8%B7%A8%E7%B6%B2%E5%9F%9F-cors-e2e5c1a53a19) > - [同源政策 (Same-origin policy) - MDN](https://developer.mozilla.org/zh-TW/docs/Web/Security/Same-origin_policy) > - [Why is the same origin policy so important? - stackexchange](https://security.stackexchange.com/questions/8264/why-is-the-same-origin-policy-so-important) ## Cross-Origin Resource Sharing (CORS) - 前面示範了一個用 Javascript 執行「跨來源 HTTP 請求」,並且被瀏覽器預設的 CORS policy 擋下來的例子: ![picture 1](https://i.imgur.com/rxXECZ4.png) **解法是什麼呢?** - 若想要你的 website 能夠取得**其他來源的伺服器資源**,會需要該伺服器回傳指定的 HTTP headers,以讓瀏覽器檢查伺服器回傳的 HTTP headers 是否符合 CORS 標準 :::warning 簡單來說,需要該伺服器實作一種「白名單機制」 ::: **那伺服器應如何實作此種白名單機制呢?** - 還需要了解,瀏覽器會對跨來源 HTTP 請求分成兩種情境、皆有不同的 CORS 驗證行為: - **Simple requests (簡單請求)** - **Preflighted requests (預檢請求)** - 伺服器需要對不同情境具體地做出正確的實作,以滿足瀏覽器的要求 > See also > - [跨來源資源共用 (CORS) - MDN](https://developer.mozilla.org/zh-TW/docs/Web/HTTP/CORS) > - [CORS Tutorial: A Guide to Cross-Origin Resource Sharing - auth0.com](https://auth0.com/blog/cors-tutorial-a-guide-to-cross-origin-resource-sharing/) ### Simple Requests - 你發起的請求只是 `GET` 與 `POST`、且沒什麼特別的 headers 時,瀏覽器會幫你執行 **simple requests** :::warning 所謂「沒什麼特別的 headers」,是指 request 若僅包含以下的 headers,就屬於 simple requests - `Accept` - `Accept-Language` - `Content-Language` - `Content-Type` (且僅是下面三種 MIME types) - `application/x-www-form-urlencoded` - `multipart/form-data` - `text/plain` - `Range` ref: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simplerequests ::: - 此時,瀏覽器真的會發送請求給伺服器,並**在拿到 response 後檢查伺服器是否允許此網頁存取它** - 檢查方式為查看 response header 中的 `Access-Control-Allow-Origin` 是否與網頁的 origin 相符合 ```mermaid sequenceDiagram participant b as Browser (foo.example.com) participant s as Server b->>s: GET /resources HTTP/1.1<br> Origin: foo.example.com s->>b: HTTP/1.1 200 OK <br> Access-Control-Allow-Origin: foo.example.com Note over b: CORS 驗證通過,正常處理資源 ``` - 若不符合,就顯示 `blocked by CORS policy` ```mermaid sequenceDiagram participant b as Browser (foo.example.com) participant s as Server b->>s: GET /resources HTTP/1.1<br> Origin: foo.example.com s->>b: HTTP/1.1 200 OK <br> Access-Control-Allow-Origin: bar.example.com Note over b: CORS 驗證不通過,<br>顯示 blocked by CORS policy ``` :::info - :memo: demo steps - `make run-simple-request` and start live coding - 實作 endpoint,只 echo request headers - go to https://example.com/ and open **DevTool** - 發起 `GET` 請求,製造一個 simple request,呈現 blocked by CORS - 完善 endpoint (`res.setHeader("Access-Control-Allow-Origin", "https://example.com")`),以允許網頁存取 ::: > See also > - [CORS: Simple requests - MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests) ### Preflighted Requests - 其他會對 server data 產生 side-effects 的請求,CORS 規範要求瀏覽器必須 1. 先 **preflight a request** 詢問伺服器:這是我接下來要傳送的 requests 摘要,請問允許嗎? 2. 若允許,才執行真實的 request - 所以一個 preflighted request,實際上會對伺服器發送 **2 次 requests** 來完成網頁的需求 **瀏覽器 preflight a request 是什麼意思呢?** - 發送一個 **HTTP `OPTIONS` 請求**給伺服器,並校驗伺服器的回傳 :::warning 其中包含兩個 **`Access-Control-Request-*`** headers: - `Access-Control-Request-Method: <method>` 描述真實請求的 method - `Access-Control-Request-Headers: <field-name>[, <field-name>]*` 描述真實請求中會帶入哪些 headers ::: **伺服器應如何正確回應 `OPTIONS` 請求給瀏覽器呢?** - 針對那些預期會被非同源存取的 endpoint,具體實作 `OPTIONS` 的回應 :::warning 伺服器得回傳一些 **`Access-Control-Allow-*`** headers 資訊讓瀏覽器校驗: - `Access-Control-Allow-Origin: <origin> | *` - 描述合法的網域為何 - `Access-Control-Allow-Methods: <method>[, <method>]*` - 描述合法的 HTTP 方法 - `Access-Control-Allow-Headers: <header-name>[, <header-name>]*` - 描述合法的標頭 ::: **一個 Preflighted request 完整的循序圖** ```mermaid sequenceDiagram participant b as Browser participant s as Server RECT rgb(37, 150, 190) Note right of b: Preflighted request b->>s: OPTIONS /doc HTTP/1.1 <br> Origin: https://example.com <br> Access-Control-Request-Method: POST <br> Access-Control-Request-Headers: Content-Type s->>b: HTTP/1.1 204 No Content <br> Access-Control-Allow-Origin: https://example.com <br> Access-Control-Allow-Methods: POST <br> Access-Control-Allow-Headers: Content-Type END RECT rgb(247, 151, 25) Note right of b: Actual request b->>s: POST /doc HTTP/1.1 <br> Origin: https://example.com <br> Content-Type: application/json s->>b: HTTP/1.1 200 OK ... END ``` :::info - :memo: demo steps - `make run-preflighted-request` and start live coding - 實作一個 `POST` endpoint 先滿足 simple request,then go to https://example.com/ and open **DevTool** - 將請求改成:`POST` + `Content-type: application/json`,製造出 preflighted request - 實作 `options` endpoint,從伺服器端看到瀏覽器真的發起了一個 HTTP `OPTIONS` 請求 - 來個 TDD,一步一步滿足瀏覽器的要求 - 完成 `options` endpoint,使伺服器正確回應瀏覽器的 preflighted request ``` res.setHeader("Access-Control-Allow-Origin", "https://example.com"); res.setHeader("Access-Control-Allow-Headers", "Content-Type"); res.send(); ``` ::: > See also > - [CORS: Preflighted requests - MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#preflighted_requests) ### Practical Discussions #### 🤔 Should I implement CORS support from scratch on server-side? - 當然找找與你使用的 web framework 最契合的 library/package,配置設一設就好囉 > See also > - [expressjs/cors](https://expressjs.com/en/resources/middleware/cors.html) for Node.js > - [gin-contrib/cors](https://github.com/gin-contrib/cors) for Go #### 🤔 Can we allow multiple origins? - `Access-Control-Allow-Origin` 並不支援回傳多個 origins 的語法 (無論你空白分隔還是逗點分隔,瀏覽器都不理你) - 🛑 `Access-Control-Allow-Origin: foo.com bar.com` - 🛑 `Access-Control-Allow-Origin: foo.com, bar.com` - 也無法透過設置多個 `Access-Control-Allow-Origin` 在 response header 中來達成 - 🛑 > ``` > Access-Control-Allow-Origin: foo.com > Access-Control-Allow-Origin: bar.com > ``` - 建議的做法是伺服器檢查 `Origin` 是否在你的白名單 - 是的話則在 `Access-Control-Allow-Origin` 中填入該 `Origin` > See also > - [Reason: Multiple CORS header 'Access-Control-Allow-Origin' not allowed - MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors/CORSMultipleAllowOriginNotAllowed) #### 📝 Google Cloud Storage (GCS) needs CORS setting - 我們會把檔案、圖片放到 GCS 內指定的 **bucket**,且可以取得一個 public URL 來指向該檔案 - format: `https://storage.googleapis.com/<bucket>/<object>` - e.g. https://storage.googleapis.com/test-origin-jubo-image/module/orgid/xxxooo - 如果該檔案是一個圖片檔,實務應用會將它直接塞進 `<img src="image link">` tag 裡,讓瀏覽器直接發出請求、拿到圖片、直接呈現 - 但 GCS 預設也不是任何人拿到 URL 都可以存取資源,它會要求你替該 bucket 設定好 CORS,正向表列出有哪些 websites 可以存取你的資源 > See also > - [Configure cross-origin resource sharing (CORS) on a bucket](https://cloud.google.com/storage/docs/configuring-cors) #### 📝 CORS-RFC1918: protect users from cross-site request forgery (CSRF) attacks targeting routers and other devices on private networks - 前面的 demo ,都刻意在 https://example.com 上,向本機伺服器 (`localhost:8080`) 發起請求 ![](https://i.imgur.com/aQBfsNO.png) - 但如果你使用 Chromium-based browser 在 http://example.com 上發起,就會出現以下的 CORS 警示 ![](https://i.imgur.com/LSCzpLy.png) - 因為,這個行為太像是一種低階的 **Cross-site Request Forgery (CSRF)**! - 會出現 CORS 警示,其機制是因為 Chromium-based browser 實作了 CORS-RFC1918,[目的](https://wicg.github.io/private-network-access/#goals)是要**保護運行在私有網路的服務、設備**,不容易被用戶代理 (user agent, i.e. browser) 利用、攻擊 :::warning - Chrome 除了已限制網站向私有網路發起請求的能力 (since Chrome 9x),並[計畫](https://developer.chrome.com/blog/private-network-access-preflight/#rollout-plan)自 Chrome 102 起擴充 CORS 的驗證行為,在 public to private 的 CORS 請求中增加 `Access-Control-Request-Private-Network: true` ,並期待私有網路內的伺服器明確地回應 `Access-Control-Allow-Private-Network: true`,才允許 CORS ::: > See also > - [Draft: Private Network Access (aka. CORS-RFC1918)](https://wicg.github.io/private-network-access/) > - stackoverflow issue > - [Chrome CORS error on request to localhost dev server from remote site](https://stackoverflow.com/questions/66534759/chrome-cors-error-on-request-to-localhost-dev-server-from-remote-site) > - 摘要文章 > - [Private Network Access: introducing preflights](https://developer.chrome.com/blog/private-network-access-preflight/) > - [RFC about CORS-RFC1918 (from a Chrome-team member)](https://web.dev/cors-rfc1918-feedback/) > - [Chrome 安全策略 - 私有网络控制(CORS-RFC1918)](https://cloud.tencent.com/developer/article/1809996) ## References - https://web.stanford.edu/class/cs253/