網頁程式設計三兩事 - 萬惡的 Same Origin 與 CORS
Preface
我最初遇到 CORS 的問題是在我的個人部落格上面,因為我引用了其他網站的圖片
某一天我突然發現圖片跑不出來了? 思來想去我應該也沒有改到程式碼才對
後來看了一下發現好像是 CORS 的問題
所以今天要來講講 CORS
Same Origin Policy
來源相同的定義為何?
- 協定一樣
- port 一樣
- domain 一樣(包含 sub domain)
只要上述條件都符合即代表相同來源
反之違反任一條件即為不同來源
網站的同源政策主要就是為了避免不同來源能夠透過 Document Object Model(DOM) 存取機密資料(e.g. cookies, session)
因此可以得知 Same Origin Policy 主要是實作在瀏覽器上面的
你說為什麼不同來源能夠存取 cookie?
那是因為早期的瀏覽器實作,即使是不同來源,它依然會帶 cookie 過去
就也因此可能會造成一些資安風險
所以引入了 Same Origin Policy 之後,可以有效的避免上述的事情發生
Introduction to CORS
CORS - Cross-Origin Resource Sharing 跨來源資源共用定義於 WHATWG’s Fetch Living Standard
CORS 不是一種安全機制,相反的,他是一種能夠突破 Same Origin Policy 限制的東西
有時候我們覺的 Same Origin Policy 太嚴格了,一些大網站用了不同的 sub domain 也被視為是不同來源實屬有點麻煩
因此 CORS 能夠使不同來源的要求被存取
Define Origin
那我要怎麼確定我的來源是誰
網站會使用 第一個 request
來確認你的來源
ref: 跨來源資源共用(CORS)
How does CORS Work
前面有提到,普遍瀏覽器為了安全問題,都有實作 Same Origin Policy
為了有效的放寬此政策,便引入 CORS 的機制
允許部份的 origin 可以存取
如果瀏覽器要發起一個 cross origin request
它會先發一個 Preflight Request 跟目標 server 確認
server 會回傳一系列的 header 來描述哪些 request 可以被接受,可以被支援
確認可以支援之後,才會發起正式 request
cross origin request 不限於 api call, <img>, <script> 如果不同來源,也都算 cross origin request
你可以發現,基本上 CORS 不會管你不同來源是否合法
它只是要確認說 server 有支援你的 request 而已
安不安全跟它沒關係
所以基本上,如果你碰到 CORS 的問題,是你的後端需要做處理
注意到 CORS 的請求,預設是不會帶身份驗證相關的資料的(i.e. Authorization)
只有當 server 回傳特定 CORS header 它才會帶
設定的部份可參考 CORS Headers
Modify Origin Header
既然 Same Origin Policy 是用於保護網站被其他網站存取
而且他依靠的是 origin header
那有沒有可能我手動把它改掉,bypass 這個限制?
基本上你沒有辦法透過手動修改 origin header 來 bypass 這個限制
瀏覽器帶的 origin header 是不可更改的
但是你可以透過其他方式來達到這個目的
比如說 proxy
Nginx 的 proxy_set_header 可以新增或修改 header
以這個例子來說就是要修改 origin header
不過這種做法算沒必要
他的前提是你要能夠操作 server
以攻擊者的角度來說,其實使用 CSRF 來做攻擊更有效率
CORS Headers
這裡就大概列出幾個常用常見的 header
完整的 header list 可以造訪 HTTP 回應標頭
Value | Description | |
---|---|---|
Access-Control-Allow-Credentials | true |
是否允許帶 credential(e.g. cookies, authorization header, tls certificate) |
Access-Control-Allow-Headers |
* Content-Type, Accept
|
允許的 header |
Access-Control-Allow-Methods |
* GET, POST, PATCH
|
允許的 HTTP methods |
Access-Control-Allow-Origin |
* https://example.com (僅允許 https://example.com)null (null origin) |
允許的 request 來源 |
Access-Control-Max-Age | 86400 |
預檢後多久以內不需要在檢查 |
Credential with Wildcard Origin
當你的 origin 設為 wildcard 而且 credential 又為 true 的時候,會出現錯誤
Access-Control-Allow-Origin: '*'
注意到,這裡的 credential 不是 Access-Control-Allow-Credentials
它說的是 XMLHttpRequest 裡面的 withCredentials
的設定(e.g. Angular - HttpRequest)
那麼他有兩種解決方案
-
withCredentials
設定不能為true
(default 為 false) - explicit 設定 origin header
Access-Control-Allow-Origin: 'http://localhost:4200'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Injectable({
providedIn: 'root',
})
export class HttpInterceptorService implements HttpInterceptor {
constructor(private userStore: Store<{ user: UserState }>) {}
intercept(
req: HttpRequest<any>,
next: HttpHandler
): Observable<HttpEvent<any>> {
return this.userStore.select('user').pipe(
first(),
mergeMap((userState) => {
req = req.clone({
setHeaders: {
Authorization: userState.Token,
},
withCredentials: true <---
});
return next.handle(req);
})
);
}
}
null origin
不建議使用 null origin
因為如果 request scheme 非 http(e.g. data:
, file:
)
這些 scheme 的 origin 會預設為 null
那這樣就會有點危險,所以一般不建議這樣設定
Request Types
Simple Request
HTTP 的 request 當中,不受限於 Same Origin Policy 的 request 被稱之為 simple request
也就是說,符合下列規則的 request 不需要套用 CORS header 即可正常請求
Methods | GET HEAD POST |
Headers | Accept Accept-Language Content-Language Content-Type Range |
Content-Type |
application/x-www-form-urlencoded multipart/form-data text/plain
|
Preflight Request
預檢請求,亦即在正式 request 之前必須要先額外發一個 request 進行檢查
根據 server 的回應,來判斷是否可以往下執行
哪些 request 會屬於 preflight 的呢? 簡單來說就是 非 Simple Request 的都是
上圖是一個完整 preflight request 的示意圖
可以看到 http://localhost:8888/me
的 request 被 call 了兩次
其中第一行即為 preflight request(他的 type 為 preflight)
如果你在 developer tools 沒有看到 preflight request, 記得把 filter 設為 all 才可以
做為 demo, http 401 可以先忽略(他是正常行為)
執行 preflight 的方式是
使用 HTTP Options method 並帶上一些 request header 進行檢查
Access-Control-Request-Headers: Authorization
Access-Control-Request-Method: GET
Options Method 為 safe method, 詳細可以參考 重新認識網路 - HTTP1 與他的小夥伴們 | Shawn Hsu
HTTP header field 並無大小寫之區分,可參考 RFC 1945 §4.2
這隻 API 主要會根據 user jwt token 取出對應 user 資料並回傳
所以他的 headers 帶了 authorization, 因為 credential 是存於 cookie 當中的
然後 server 這邊就要回應,它能夠處理的 CORS header 有哪些
有 4 個
-
Access-Control-Allow-Credentials
允不允許 request 帶 credential -
Access-Control-Allow-Headers
允許哪些 HTTP header -
Access-Control-Allow-Methods
允許哪些 methods -
Access-Control-Allow-Origin
允許特定 request origin(i.e. 從哪裡來)
當所有條件都符合,都 ok,status code 會是 HTTP 204 No Content
你的 web browser 就會放行,進行真正的 request
CORS in Postman?
CORS 的問題基本上是為了解決瀏覽器實作的 Same Origin Policy
也因此,在你 debug 的時候使用 postman 或是 curl
CORS 的問題基本上不會出現(因為它不在瀏覽器裡面跑)
Refferer Policy: strict-origin-when-cross-origin
有的時候不是你設定有錯,是瀏覽器的問題
假設你 後端 正確的設定了 CORS header 了,但是你還是遇到問題
八成是瀏覽器在搞事,舉例來說 Google Chrome
Chrome 的選項 Block insecure private network requests
記得要把它關閉
然後你的網站就可以正常運作了
Configure CORS Support in Golang Gin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import (
"net/http"
"github.com/gin-gonic/gin"
)
func CorsMiddleware() gin.HandlerFunc {
return func(ctx *gin.Context) {
allowHeaders := `
Content-Type, Content-Length,
Authorization, Accept,
Accept-Encoding, Origin,
DNT, User-Agent,
Referer
`
allowMethods := `
POST, GET, PUT,
DELETE, PATCH, OPTIONS
`
ctx.Writer.Header().Set("Access-Control-Allow-Origin", "*")
ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
ctx.Writer.Header().Set("Access-Control-Allow-Headers", allowHeaders)
ctx.Writer.Header().Set("Access-Control-Allow-Methods", allowMethods)
if ctx.Request.Method == "OPTIONS" {
ctx.AbortWithStatus(http.StatusNoContent)
return
}
ctx.Next()
}
}
Referer 這個字其實是有故事的,可參考 HTTP 協定的悲劇
基本上就是定義你允許的各種條件
像是 origin 為 wildcard 代表你允許所有來源
allow credentials 可以允許攜帶 token 之類的東西
允許的 header 以及 method
為了處理 preflight request 不要出錯
會刻意抓 options request 出來,因為我們 router 並沒有定義相關 routing
沒有特別處理他到後面會 404 not found
Gin 其實有一套 CORS 的 library gin-contrib/cors
可以比較簡單的設定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main
import (
"time"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
// CORS for https://foo.com and https://github.com origins, allowing:
// - PUT and PATCH methods
// - Origin header
// - Credentials share
// - Preflight requests cached for 12 hours
router.Use(cors.New(cors.Config{
AllowOrigins: []string{"https://foo.com"},
AllowMethods: []string{"PUT", "PATCH"},
AllowHeaders: []string{"Origin"},
ExposeHeaders: []string{"Content-Length"},
AllowCredentials: true,
AllowOriginFunc: func(origin string) bool {
return origin == "https://github.com"
},
MaxAge: 12 * time.Hour,
}))
router.Run()
}
ref: gin-contrib/cors
References
- 跨來源資源共用(CORS)
- What is the issue CORS is trying to solve?
- 同源政策 (Same-origin policy)
- Cross-origin resource sharing
- If browser cookies aren’t shared between different websites, then why is Same origin Policy useful?
- [Day 26] Cookies - SameSite Attribute
- Does every web request send the browser cookies?
- how exactly CORS is improving security [duplicate]
- In CORS, Are POST request with credentials pre-flighted ?
- Why is jQuery’s .ajax() method not sending my session cookie?
- What’s to stop malicious code from spoofing the “Origin” header to exploit CORS?
- Same-origin policy
- CORS - Is it a client-side thing, a server-side thing, or a transport level thing? [duplicate]
- Reason: Credential is not supported if the CORS header ‘Access-Control-Allow-Origin’ is ‘*’
- CORS error on request to localhost dev server from remote site
Leave a comment