前言
2019 年我在 Medium 上發布過一篇關於 Cookie 與 CORS 的文章(和 Cookie 與 CORS 打交道),受到不錯迴響,文章甚至還被抄到對岸。希望大家記得原作者在這裡就好。當時的文章在探討 CORS 是什麼,以及在前端、後端上要如何正確實作 CORS,是網頁開發中時常碰到的問題。
又過了一年多,Chrome 發布了 Cookie 政策改變(將 SameSite 預設改為 lax),我開始重新思考這件事,並且寫下了感想在 Chrome Cookie 政策調整與反思這篇文章當中,闡述我對 SameSite
的疑問。其中有兩個問題我還在尋找解答:
- 所以當 Server 加入
SameSite
之後,就不需要實作 CSRF 機制了嗎?如果使用者使用老舊瀏覽器(非支援 SameSite Attribute)造成安全性問題,請問這個安全性問題算公司的責任還是瀏覽器的責任? - 使用
Cookie
真的是一個滴水不漏的方法嗎?
關於第一個問題,我想隨著時代演進,應該會有越來越多瀏覽器跟上時代,到時候應該就可以完全汰換掉 CSRF 機制?
第二個問題是因為看過幾個案例而有的質問,讓我在這邊多闡述一下:
在這之前先來一個小測試,請問在 cookie 當中設定 domain=.example.com 跟 domain=example.com 的作用範圍為何?
我們等下再來看答案,先來看看一個經典案例 - Cookie Bomb
1. 什麼是 Cookie Bomb?💣
在文章當中使用了 Github Page 來示範 Cookie Bomb。
Github 除了可以放程式碼之外也可以拿來做成 Github Page 當作個人網頁或部落格使用,而 github 會同時給你一個 user.github.io
的網域名稱。(在早期是直接在username.github.com
,可參考本篇文章)
一般的瀏覽器當中,一個 Cookie(包含 key, value, expires 等),可以放入 4K 的資料,最多可以放 50 個(根據瀏覽器可能略有差異),加起來最多總共可以到 4KB * 50 = 200KB。。Cookie Bomb 的手法就是在客戶端用 document.cookie
的方式寫入大量 Cookie,因為 Cookie 預設會包含在所有請求中,200KB 對於一個 GET 請求來說是相當巨大的,所以一般在 Server 端或是在 nginx 層就會先被擋住,直接忽略請求。
接下來是最大的問題在這裡,Cookie 會自動套用的所有子網域當中(有些條件例外),所以當你在 A 網頁受害(被灌滿 Cookie 之後),也會同時在 B 網頁 C 網頁生效。導致你看所有的 Github Page 都像是壞掉一樣。
嚴格來說,這個 Cookie Bomb 並沒有造成安全性問題,只是有點搞怪而已,受害者也只要把大量的 Cookie 清掉就可以讓請求正確被 Server 處理,但像這種部落格的服務 Cookie 彼此共享聽起來還是怪怪的吧。
為了避免這個問題,在某些網域當中的子網域,彼此不會彼此共享 Cookie,叫做 public suffix list,由 Molliza 所維護,可以在裡頭找到所有 public suffix 的網域清單。
回到剛才的問題,domain=.example.com
跟 domain=example.com
是一樣的,都會做用到全部的子網域當中。
domain 沒有設定的話至少預設還會是限制在相同 domain 當中,設定了之後直接將 cookie 送到所有子網域。
2. 你真的知道 Cookie 是如何運作的嗎?
現在的瀏覽器應該都是按照 RFC6265 的標準實作 Cookie。例如 Cookie 遇到相同值應該怎麼處理?Cookie 是否需要排序?
就我目前的經驗來說,能理解 Cookie 各種奇妙的歷史,並且知道規格細節的工程師極為稀少,我認為這是很正常的事。一般的工程師知道 Cookie 怎麼設定,httpOnly
跟 secure
的運作方式,path 與 domain 的邏輯,大概知道這些就夠用了。畢竟要打造產品總不可能還要閱讀完所有的規格書,理解 Cookie 的來龍去脈後才能開發吧。
就算以上這些都被成功防護了,還要再看看你使用的框架本身是如何解析與實作 Cookie 的,才能做到百分之百安全。
重新思考:另外一種可能性
基於以上的種種案例,讓我開始思考了什麼是最佳應用,反而讓我把焦點放到了 JavaScript
當中,接下來看看 JavaScript
當中是怎麼解決這些問題的。
我在上篇文章中也有提過,當時參考的文章是這篇:Cookies are bad for you,在這邊複習一下。
由 JavaScript 送出的請求,像是 ajax 或是 fetch,被定義成一個 CORS 請求。只要是 CORS 請求就會有一些條件與限制,詳細可以到我之前寫的文章或是到 MDN 做參考,在這裡來描述一下 CORS 是如何保護你的請求:
- 跨網域請求時預設 Cookie 不會自動送出 / 接收:這點防止了 CSRF,因為 CORS 請求來自 JavaScript 程式碼,所以一般的 CSRF 攻擊無法生效。
- CORS 需要 JavaScript 執行:我們可以讓驗證機制放在
Authorization
這個 Request Header 當中,一般的 form 表單與超連結攻擊沒辦法加入 request header 的關係,因此可以有效防止 CSRF。 - 可在請求中加入 Header:fetch 可以動態加入 header 來做身份驗證機制。例如:
fetch('/api', {
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token')
}
})
- 如果驗證不走 Cookie 機制:不需要釐清複雜的 Cookie 規格,也不用擔心 CSRF 實作問題。
好,大家最想問的問題來了:
token
放localStorage
如果被 XSS 不就完蛋了?- 如果使用者沒有開啟
JavaScript
不就沒辦法運作?
關於問題 1. 我其實也還在思考這個問題,但似乎沒有人回答過:「所以如果用 Cookie 然後被 XSS 的話就不會完蛋了嗎?」
差別在於攻擊者是否能夠拿到 token,我們不妨思考 XSS 後會發生的事:
- Cookie: XSS 發生 → 攻擊者插入惡意程式碼 → 擷取資料
- localStorage:XSS 發生 → 攻擊者拿取 token → 擷取資料
我看到現在的討論都只停留在第二步,卻沒人討論說既然造成的結果是一樣的,那麼兩者本質上的差異到底在哪裡,這是我覺得相當可惜的一部份。
每個人都覺得 Cookie 就是讚、就是安全,看到 localStorage 就大力譴責,但事實上真的如此嗎?Cookie 真的滴水不漏嗎?規格上的複雜度是否造成了更多潛在問題?反而沒有看到很多人討論這一塊。
關於問題 2. ,我會覺得既然要走 localStorage 的機制,當然就要付出一些代價,事實上大部分的使用者都會開啟 JavaScript 來瀏覽網頁,如果不允許執行 JavaScript 的話,我想大部分沒有實作 SSR 的 SPA 網頁們都要直接壞掉了吧。
後記
雖然說我對 Cookie 有著一些質疑,也不知道框架上的實作是否足夠穩健,但我覺得 Cookie 的規格演變到現在已經算是越來越穩定跟安全;框架的部分只要是開源的話,也應該是足以被信任才對。SameSite
的實作也幾乎可以適用到大部分的瀏覽器,所以應該是個蠻有希望的未來,不過還是希望可以得到更深一點的討論與激盪,因此寫了這篇文章。
以上為我自己對 Cookie 的理解,如果有任何錯誤之處歡迎指正。
補充
文章發佈於社團後有一些建議,可以到留言區看看。