---
# System prepended metadata

title: Web Security 101 - Same Origin Policy and Cross-Origin Resource Sharing (CORS)
tags: [tutorials, web security, jubo]

---

---
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/