Cookie 固然方便,但畢竟它存在著安全隱患(security pitfalls),使得我們無法隨意地將極機密的資訊保存在 Cookie。
RFC 6265 section 8 提到了 Cookie 的安全注意事項,這都是在做身分驗證需要考慮的問題。
RFC 裡提到,Cookie 是一種 Ambient Authority 的形式。如果太依賴 Cookie 做為身分驗證手段的話,會有安全漏洞。主要是因為 User Agent 發送請求時,只要符合 Set-Cookie
的條件,就會夾帶當初設定的資訊。這可能會讓使用者在不知情下,以該使用者的身分執行某些攻擊者想騙使用者做的事。
這問題所引發最常見的攻擊手法即為 Cross-site request forgery,也縮寫作 CSRF 或 XSRF。比方說,Apple 買 iPhone 11 的連結如下:
https://apple.com/buy/iphone11?to=miles
攻擊者只要到處散布下面這個連結到各大社群,只要有人登入狀態還未清除,又不小心點擊到,攻擊者就會收到一隻新的 iPhone 11。
https://apple.com/buy/iphone11?to=attacker
這例子對攻擊者來說,執行起來非常簡單;相對地,對服務器來說,即便做了傳輸層加密,同時也做了 Cookie 屬性設置,依然還是會讓攻擊者得逞。
解決此問題的基本概念是:不能完全依賴 Cookie 作為身分認證方法,必須要額外加上其他認證方法。比方說,在官方的輸入表單產生一個攻擊者無法取得的亂數驗證碼如下:
<input type="hidden" name="_csrf" value="A12DD3BA-2C9B-4D3E-BC3A-8B3143E45B83"/>
在接收表單的時候去確認此亂數驗證碼是剛剛產生的。這樣若是不小心點擊到攻擊者散布的連結,會發生驗證碼比對錯誤,即可避免使用者踩中攻擊者的陷阱。
除非有使用安全通道(secure channel),不然 Cookie 的內容會是用明文傳輸。如果明文傳輸就會有以下問題:
RFC 裡的建議是,服務器應該對 Cookie 做加密(encrypt)和簽章(sign),即便是使用安全通道也是。只是即便加密和簽章,也無法防止重送攻擊。
需注意的是,如果有使用安全通道的話,則 Cookie 就得加上 Secure 屬性,不然安全通道的保護就沒有意義了。RFC 舉的例子是,攻擊者只要攔截 HTTP 請求,然後重導向使用者到沒有 Secure 屬性站台,並使用 HTTP 協定。即便站台沒有開啟 HTTP 連接埠,User Agent 還是會將該 HTTPS 的 Cookie 夾帶到 HTTP 的請求裡,攻擊者再攔截下來重送到該服務器上,就能偽造該使用者身分進入服務了。時序圖如下:
@startuml
participant Alice
participant Attacker
participant Bob
Alice -> Bob: Request
Bob -> Alice: Response: Set-Cookie: SID=31d4d96e407aad42
Alice --> Attacker: Intercept HTTP Request
Attacker --> Alice: Response: Redirect to Bob by HTTP
Alice --> Attacker: Intercept HTTP Request with Cookie
Attacker --> Bob: Request with Alice Cookie
@enduml
所有狀態都放在 Cookie,並不是很恰當,一來 Cookie 有容量限制,二來有些機密資訊不適合被保存到 User Agent。因此有一種做法是,第一次來網站,就先發給 User Agent 一個 Cookie 與值,代表一個鑰匙。服務器拿到鑰匙就去打開背後對應的儲存空間,並把機器資料放裡這個儲存空間裡。這時只要儲存空間外面是接觸不到的,就能提高機密資訊的安全性。
時序圖如下:
@startuml
Alice -> Bob: Request: SID=31d4d96e407aad42
Bob -> Storage: Key: 31d4d96e407aad42
Storage -> Bob: Value: Alice secret
@enduml
只是即便如此,還是無法避免 CSRF 或重送攻擊等攻擊手法,而且並且會有其他的攻擊手法如下:
第一種最簡單好理解,當攻擊者猜到 Session ID 就可以立馬以該使用者的身分使用服務。提升安全性的做法是使用適合的亂數產生器與適當的長度,提升攻擊者猜測的難度。
第二種是偷別人的 Session ID,只要偷到了,就能代表該使用者使用服務。經典的攻擊手法像是 Cross-site scripting(XSS) 讓使用者在不知不覺中執行攻擊者的指令碼;而指令碼實際做的事可能就是把 Session ID 傳送給攻擊者的主機。提升安全性的做法是,使用安全通道加上適當的 Cookie 屬性(如 HttpOnly
)與 Content Security Policy(CSP) 限制網頁執行 Javascript 的範圍。
第三種則是利用系統 Session ID 固定不變的機制,來欺騙使用者使用攻擊者的 Session ID 登入,使用者登入後,攻擊者即可用使用者登入過的 Session ID 進入系統。攻擊手法的時序圖如下:
@startuml
participant Alice
participant Attacker
participant Bob
Attacker --> Bob: Request
Bob --> Attacker: Response: Set-Cookie: SID=31d4d96e407aad42
Attacker --> Alice: Fake link to using SID=31d4d96e407aad42 login
Alice -> Bob: Request: Cookie: SID=31d4d96e407aad42
Bob -> Alice: Response: SID=31d4d96e407aad42 Login success
Attacker --> Bob: Request: Cookie: SID=31d4d96e407aad42
Bob --> Attacker: Response: SID=31d4d96e407aad42 Login success
@enduml
提升安全性的做法是:系統在未登入轉換到登入後,應該要原本的 Session ID 消滅(如上例的 SID=31d4d96e407aad42
),並產生新的 Session ID。如此一來,攻擊者嘗試使用原本的 Session ID 將會得不到任何資料。
若想在 Cookie 放機敏資訊,RFC 這裡給的提醒是--千萬不要這麼做!Cookie 不能保證存在裡面的東西不會洩露出去。
RFC 裡面舉的例子就很清楚:雖然不同的 domain 或 path 可以設定不同的 Cookie,但不同的連接埠則會共用同一份 Cookie。這代表不能在同個 domain 上,執行兩個不能互相信任且不同連接埠的服務。
Cookie 對於主網域與子網域的資訊,無法保證其完整性。
RFC 裡提到的一個例子是:foo.example.com 可以設定 example.com 的 Cookie,而在 bar.example.com 在取得 Cookie 時,會無法確認這個 Cookie 是 foo 子網域設定的還是 bar 子網域設定的。在這前提下,foo 子網域就能利用這個漏洞對 bar 子網域發出攻擊。
雖然加密或簽章能保護資料不被竄改,但一樣還是會有重送攻擊的問題。
簡介 Cookie 提了許多設定的例子,以及上面討論的某些問題,都是在 domain 上設定 Cookie,代表 Cookie 協定對 DNS 有某種程度的依賴關係。因此 DNS 只要被攻破,Cookie 的資料將會傳送到攻擊者的服務器裡。
Cookie 設定非常方便,但也帶來許多資安問題。好在 RFC 對安全注意事項都會說明的非常清楚,在決定使用某項協定前,預先看過一輪是必要的,尤其是做身分驗證。