Try   HackMD
tags: jubo tutorials web security

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
demo 須知

Same Origin Policy

What is "Origin"?

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

See also

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) 示範 tag 會被允許、而 JS code 會被 CORS 阻擋的例子:
<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>

demo steps

  • make run-same-origin-policy-demo
  • open localhost:3000
  • open DevTool and see the console

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

See also

Cross-Origin Resource Sharing (CORS)

  • 前面示範了一個用 Javascript 執行「跨來源 HTTP 請求」,並且被瀏覽器預設的 CORS policy 擋下來的例子:
    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →

解法是什麼呢?

  • 若想要你的 website 能夠取得其他來源的伺服器資源,會需要該伺服器回傳指定的 HTTP headers,以讓瀏覽器檢查伺服器回傳的 HTTP headers 是否符合 CORS 標準

簡單來說,需要該伺服器實作一種「白名單機制」

那伺服器應如何實作此種白名單機制呢?

  • 還需要了解,瀏覽器會對跨來源 HTTP 請求分成兩種情境、皆有不同的 CORS 驗證行為:
    • Simple requests (簡單請求)
    • Preflighted requests (預檢請求)
  • 伺服器需要對不同情境具體地做出正確的實作,以滿足瀏覽器的要求

See also

Simple Requests

  • 你發起的請求只是 GETPOST、且沒什麼特別的 headers 時,瀏覽器會幫你執行 simple requests

所謂「沒什麼特別的 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 相符合
      ServerBrowser (foo.example.com)ServerBrowser (foo.example.com)CORS 驗證通過,正常處理資源GET /resources HTTP/1.1 Origin: foo.example.comHTTP/1.1 200 OK  Access-Control-Allow-Origin: foo.example.com
    • 若不符合,就顯示 blocked by CORS policy
      ServerBrowser (foo.example.com)ServerBrowser (foo.example.com)CORS 驗證不通過,顯示 blocked by CORS policyGET /resources HTTP/1.1 Origin: foo.example.comHTTP/1.1 200 OK  Access-Control-Allow-Origin: bar.example.com
  • Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →
    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

Preflighted Requests

  • 其他會對 server data 產生 side-effects 的請求,CORS 規範要求瀏覽器必須
    1. preflight a request 詢問伺服器:這是我接下來要傳送的 requests 摘要,請問允許嗎?
    2. 若允許,才執行真實的 request
  • 所以一個 preflighted request,實際上會對伺服器發送 2 次 requests 來完成網頁的需求

瀏覽器 preflight a request 是什麼意思呢?

  • 發送一個 HTTP OPTIONS 請求給伺服器,並校驗伺服器的回傳

其中包含兩個 Access-Control-Request-* headers:

  • Access-Control-Request-Method: <method> 描述真實請求的 method
  • Access-Control-Request-Headers: <field-name>[, <field-name>]* 描述真實請求中會帶入哪些 headers

伺服器應如何正確回應 OPTIONS 請求給瀏覽器呢?

  • 針對那些預期會被非同源存取的 endpoint,具體實作 OPTIONS 的回應

伺服器得回傳一些 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 完整的循序圖

ServerBrowserServerBrowserPreflighted requestActual requestOPTIONS /doc HTTP/1.1  Origin: https://example.com  Access-Control-Request-Method: POST  Access-Control-Request-Headers: Content-TypeHTTP/1.1 204 No Content  Access-Control-Allow-Origin: https://example.com  Access-Control-Allow-Methods: POST  Access-Control-Allow-Headers: Content-TypePOST /doc HTTP/1.1  Origin: https://example.com  Content-Type: application/jsonHTTP/1.1 200 OK ...
  • Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →
    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

Practical Discussions

🤔 Should I implement CORS support from scratch on server-side?

  • 當然找找與你使用的 web framework 最契合的 library/package,配置設一設就好囉

See also

🤔 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

📝 Google Cloud Storage (GCS) needs CORS setting

  • 我們會把檔案、圖片放到 GCS 內指定的 bucket,且可以取得一個 public URL 來指向該檔案
  • 如果該檔案是一個圖片檔,實務應用會將它直接塞進 <img src="image link"> tag 裡,讓瀏覽器直接發出請求、拿到圖片、直接呈現
  • 但 GCS 預設也不是任何人拿到 URL 都可以存取資源,它會要求你替該 bucket 設定好 CORS,正向表列出有哪些 websites 可以存取你的資源

See also

📝 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) 發起請求
  • 但如果你使用 Chromium-based browser 在 http://example.com 上發起,就會出現以下的 CORS 警示
    • 因為,這個行為太像是一種低階的 Cross-site Request Forgery (CSRF)!
  • 會出現 CORS 警示,其機制是因為 Chromium-based browser 實作了 CORS-RFC1918,目的是要保護運行在私有網路的服務、設備,不容易被用戶代理 (user agent, i.e. browser) 利用、攻擊
  • Chrome 除了已限制網站向私有網路發起請求的能力 (since Chrome 9x),並計畫自 Chrome 102 起擴充 CORS 的驗證行為,在 public to private 的 CORS 請求中增加 Access-Control-Request-Private-Network: true ,並期待私有網路內的伺服器明確地回應 Access-Control-Allow-Private-Network: true,才允許 CORS

See also

References