簡單弄懂同源政策 (Same Origin Policy) 與跨網域 (CORS)
Understanding Frontend Security 系列文
1. 簡單弄懂同源政策 (Same Origin Policy) 與跨網域 (CORS)
2. [XSS 1] 從攻擊自己網站學 XSS (Cross-Site Scripting)
3. [XSS 2] 如何防禦 XSS 攻擊
4. Content Security Policy (CSP) — 幫你網站列白名單吧
5. [CSRF] One click attack: 利用網站對使用者瀏覽器信任達成攻擊
我相信每一個前端在串接 API 時,多多少少一定遇過以下錯誤

雖然要解決此問題大部分還是需要後端幫忙,但前端卻需要知道為什麼會發生、大概要如何解決。
🔖 文章索引1. 同源政策 (Same Origin Policy)
2. CORS (Cross-Origin Resource Sharing)
同源政策 (Same Origin Policy)

The same-origin policy is a critical security mechanism that restricts how a document or script loaded by one origin can interact with a resource from another origin.
同源政策是網站安全的基礎。 https://domain-a.com 只能存取自己網站裡的資源 (圖片、影片、程式碼等),不允許網站 https://domain-b.com 來存取。想要存取跨來源資源必須在某些特定情況下才被允許。

畫清楚界線自己網站自己管,別人讀取不到也改不了。這很合理,不然若自己網站被別人任意修改、被惡意人士讀取機密資訊那就糟糕了!若沒有這層防護,壞人就可以任意新增刪除你 Facebook 裡面留言了、也可以輕易登入你銀行帳戶 😨。
怎麼判斷同不同源?
只要 scheme、domain、port 一樣就會被視為同源 (same-origin),其他則是不同源


若以 https://domain-a.com:80/hannah-lin 做範例,我們可以因此判斷跟以下是否同源
http://domain-a.com → 不同源.scheme 不同
https://domain-a.com/mike → 同源
https://news.domain-a.com → 不同源.domain 不同
https://domain-a.com:81 → 不同源.port 不同
https://domain-b.com → 不同源.domain 不同
Note. IE 對於不同 port 會視為同源
但我網站明明就引入很多跨來源的資源啊?
網頁常見的跨來源資源真的不少,例如以下


沒錯,在某些情況下跨來源是被允許的,不受同源策略限制
跨來源嵌入通常被允許 (embed)
像範例的 <script src=”…”></script>
、<link rel=”stylesheet” href=”…”>
、<iframe>
、圖片 <img>
、<video>
、或是 @font-face
<object>
、 <embed>
.等等都是跨來源嵌入。
跨來源寫入通常被允許 (writes)
可以在藉由<form>
在domain-a.com
發 request 給 domain-b.com
,當然透過連結 links 或直接 redirect 到別的網站也是被允許的。
跨來源讀取通常被禁止 (reads)
domain-a.com
不能讀取 domain-b.com
的 cookie、XMLHttpRequest ,Fetch API 也都無法被讀取,會回報錯誤

同源政策 (Same Origin Policy) 允許 HTML tag 產生的跨來源寫入 (write)/嵌入 (embed)/讀取 (read),但對於 JS 的跨來源 write/embed/read 是有限制的。
那什麼是 Cookie 的同源 ?
Set-Cookie: <cookie-name>=<cookie-value>
Set-Cookie: id=1234567; domain=hello.com
前端都知道 Cookie 是什麼,簡單來說就像一張通行證,通行證裡面會存有一組 Session ID 來驗證你身份,就像你加入某某俱樂部一樣,認卡不認人,誰擁有這張通行證就可以進入俱樂部。
Cookie 的同源跟以上所說的 DOM 同源有點不同 (圖片或是程式碼等資源被載入瀏覽器最後都會變成 DOM 的元素,延伸閱讀: 從輸入網址列到渲染畫面,過程經歷了什麼事?)

只要 domain 跟 Path 與 Cookie 上的一樣就會被視為同源。若經過一些設定才會判斷 scheme 要是 http 或 https。
// 加了 Secure 會限定此 Cookie 只能以 https 傳送
Set-Cookie: id=1234567; domain=hello.com; Secure
另外要注意的是子網域的 cookie 是可以存取到母網域 cookie 的 (
api.domain-a.com
跟domain-a.com
cookie 可以被共用)
對了,同源政策是瀏覽器專屬,所以才會發生用 postman 可以拿到 API 回應但放到網站上就是會失敗的狀況。
CORS (Cross-Origin Resource Sharing)
CORS 翻成中文就是跨網域資源共享,也就是你可以用我資源我也可以用你的

為什麼會有 CORS
Same Origin Policy 雖然不錯,因為他防止了一些惡意的 script 攻擊,但總不會每一個跨網域都是惡意的; 也不可能一間公司擁有所有的資源,有時還是必須串接第三方資源,例如 Facebook API、Google Map、政府釋出的公開 API 等。
怎麼實作
實作 CORS 相當容易,它其實只是 HTTP-Header 而已,這些設定基本上都是在後端,所以前端只需知道概念跟怎麼看 Responce Header 即可。
當前端用 fetch 或 XMLHttpRequest 要存取資源時,在 request 之前都會先發送 preflight request 確定 server 端有設定正確的相關 Http-Header,若檢查通過,才會實際發出 requeest。例如
fetch('https://xxx.com/data/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
那 request header 會大概長這樣
POST /data/
Host: xxx.com
Origin: https://myweb.com
Content-Type: application/json
preflight reques 會大概是這樣
OPTIONS /data/
Host: xxx.com
Origin: https://myweb.com/
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type
你可以在 developer tools 中的 Network 看到相關的 Responce Header

當然後端可以把權限開到最大讓任何人都可以讀取,不受同源政策的限制
Access-Control-Allow-Origin: *
但跨來源還是有許多風險的,所以建議還是不要直接 *
開好開滿。
// 還是會針對特定網站開權限
Access-Control-Allow-Origin: https://foo.example
// 可以設定允許哪些 method,defult 是全部方法
Access-Control-Request-Method: POST, GET
// 允许 client side 帶 cookie 等驗證,defult 是 false
Access-Control-Allow-Credentials: true
另外如果你需要在發送 request 的時候帶上 cookie,那必須滿足三個條件:
- 後端 Response header 有
Access-Control-Allow-Credentials: true
- 後端 Response header 的
Access-Control-Allow-Origin
不能是*
,要明確指定 - 前端 fetch 加上
credentials: 'include'
fetch('http://localhost:3000/form', {
method: 'POST',
credentials: 'include', // 前端新增這個允許 cookie
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
}).then(res => res.json())
.then(res => console.log(res))
想看更多可以直接閱讀 MDN 的 CORS
simple request?
滿足以下全部條件就是 simple request
- HTTP methods: GET、POST、HEAD
- 不帶自訂的 header
- Content-Type:
application/x-www-form-urlencoded
、multipart/form-data
或是text/plain
Access-Control-Allow-Origin: *
基本上就可以滿足 simple request 的 CORS 問題,但若不是 simple request 後端就必須再加上 HTTP methods 跟 Content-Type ,例如若 Content-Type 為 application/json
// BE 要加上 Access-Control-Allow-Headers 明確表示願意接受這個 header
app.options('/form', (req, res) => {
res.header('Access-Control-Allow-Origin', '*')
res.header('Access-Control-Allow-Headers', 'content-type')
res.header('Access-Control-Allow-Methods', 'PATCH, POST, GET, DELETE')
res.header('Content-Type','application/json')
res.end()
})
preflight request?

不是 simple request 都會先發一個 preflight request 確定 server 端有設定正確的相關 Http-Header,也就是會先問 server 是否允許這個 request,允許的話才會正式 request。像 HTTP PUT/DELETE method
,或 Content-Type: application/json
都會先發 preflight request。
有同源政策的保護,我的網站就是安全的嗎 ?
答案是: 不對。
Same Origin Policy 只是最基本的保護而已,實際上 attackers 還是可以聰明地找到漏洞攻擊你的網站。例如 Cross-site scripting (XSS) 可以欺騙網站來繞過同源政策的保護,這是一個很大的問題,因為同源政策下瀏覽器信任底下所有資料存取都是安全的,但這樣被惡意注入利用很有可能就讓你網站掛掉,更嚴重是用戶敏感資料被洩漏,應該被保密的資料被壞人利用。
HTML5 Security Cheatsheet 這個網站蠻酷的,提供各種 XSS 變形手法程式以及範例可以上去玩玩看


要確保自己網站的安全,還是需要下功夫的,接下來也會介紹一些簡單的方法來保護你的網站!