隨著使用者對隱私權的重視,各個服務也都逐漸開始調整自家的隱私權與安全,像是 Mac 改版到 Catalina 之後會很煩人地問你是否要同意 xxx 存取某某資源。
雖然安全性上的確是變得更嚴謹了,不過也因為如此有時候會出現一些慘案。像是 WACOM 手繪版沒辦法順利使用之類的。
而 Google 也在 2019 的 Google I/O 大會上宣佈了安全政策調整,其中對目前網頁開發影響最大的應該就是 Chrome 官方會逐漸移除第三方 Cookie 的支援。
其中從 Chrome 80+ 開始,會將 Cookie 當中的 samesite
attribute 預設設定為 lax
(在版本 80 之預設是 None)。接下來我們會從 samesite
的定義以及它有什麼用處,還有對 Cookie 的反思為出發點,談談我對整件事的想法。
Cookie 是什麼?
Cookie 是一個在客戶端實作的小型儲存機制(4KB),可以由 Server 端回傳的 Response Header 來控制。以往只要使用了 Cookie,整個機制的實作通常是仰賴瀏覽器端,瀏覽器會根據目前的條件以及設定的標頭來決定是否可以發送 Cookie、何時過期等等。在 Cookie 還沒有過期且符合條件的情況下,Cookie 就會自動包含在每個 request 裡頭送出。
對於無狀態的 HTTP Request 來說,Cookie 的機制讓我們可以保有一些使用者的資料,讓 Server 判斷目前的使用者狀態,或是用來做一些追蹤。
我會說 cookie 最方便同時也是最致命的機制就是這點。
在 Cookie 還沒有過期且符合條件的情況下,Cookie 就會自動包含在每個 request 裡頭送出
為什麼會這麼說呢?在一般情況下像是 <form>
<iframe>
<a>
<link>
<img>
等等,預設都會把 Cookie 送出,所以我們可以做到像是下面這些事:
-
透過
iframe
做第三方追蹤- 你登入了 google 帳號,google.com 回傳 Set-Cookie 並儲存在瀏覽器端
- 你在 B 網站瀏覽網頁,B 網站內嵌 google
iframe
做追蹤 - iframe 將 cookie 送到 google
-
透過
<img>
做追蹤- 你登入了 google 帳號,google.com 回傳 Set-Cookie 並儲存在瀏覽器端
- 你在 B 網站瀏覽網頁,B 網站在網頁載入時用
<img src="xxxx.google.com/track/pageview" />
發送一個圖片請求 - cookie 送到 google 端並做統計,得知你目前正在瀏覽的網站
-
透過
<a>
做壞壞的事- 你登入了 B 網站
- 這個網站實作很爛,刪除使用者的 URL 長得像這樣
GET /user/delete
- 有駭客寄了一封 email 給你,裡頭有個
<a href="xxx.com/user/delete">click me</a>
- 你點進去,然後帳號就被刪掉了
-
透過
<form>
做壞壞的事- 你登入了 B 網站,在 B 網站
- 你在惡意的 A 網站填寫表單,但其實是送到 B 網站,例如付款資料
- 你的付款資料提交到 B 網站,造成大量損失
第 3, 4 點是我們熟悉的 CSRF(Cross Site Request Forgery)跨站請求偽造。而防禦的方式是 1. 不要用 GET 方法做會有 side-effect 的操作 2. 使用 CSRF token 做驗證,確保請求真的是從自己信任的來源而來。
因此為了防禦 CSRF 攻擊,最常見的手法就是在 HTML 當中加入一個 CSRF Token,而 server 每次要執行操作的時候,都先檢查這個請求是不是有 CSRF Token 確保來源真的是自家的服務。
CSRF 的確解決了 Cookie 機制所造成的安全問題。對於有經驗的工程師來說,一定都會知道實作一個 stateful 的 CSRF token 的機制是多麽煩人且容易出錯的工作(尤其是在流量大的情況下),每次提到 CSRF Token 大家總是一副囧臉。
其實早在 13 多年,就有人提出不要讓像是 <img>
<link>
之類的標籤將 Cookie 送出(文章),但最後得到的解答是
The attack described here is well-known and called "Cross-site request forgery". Most believe that it is the web application's responsibility to fix it, not the web browser's.
為了解決 CSRF 帶來的問題,Chrome 51+ 開始新增了 SameSite attribute 來預防 CSRF 攻擊。它的原理把是否要發送 cookie 的選擇權交給了實作者,有三個值可以選擇:
strict
任何情況下都不發送 Cookie。雖然最安全但有時不一定會想要這樣做,例如我在 B 網站連結到 YouTube,結果因為 Cookie 沒有發送的關係會是未登入的狀態。lax
只在 GET navigate 到目標網址的情況下會發送 Cookie。none
預設發送 Cookie
Chrome 80+ Cookie SameSite 政策
最近吵得沸沸揚揚的 samesite cookie 就是指 Chrome 80+ 以後會將 SameSite=none
預設改為 SameSite=lax
,以確保使用者的安全性。
好的,這大概是背景介紹,現在來談談我的反思。
反思 1: 所以加上 SameSite=lax
就不用實作 CSRF Token 了嗎?
因為 SameSite 這個標準其實算是相對新的東西(其實也不算新了啦),但是如果使用者用的是比較老舊的瀏覽器,還沒有支援 samesite 的話是不是有可能就遭受到 CSRF 攻擊?
那麼這個政策的改變對我來說就有點雞肋,除了可以防止第三方的追蹤之外,對於自家的服務來說還是必須實作 CSRF Token 才能有效防止攻擊。
事實上 Cookie 在各家瀏覽器當中的實作其實也不太一樣,因為 Cookie 所造成的安全問題也不少,像是:
- Safari 12 之前允許用 JavaScript 改寫 httpOnly 的 Cookie
- 規範複雜(domain=.example.com 跟 domain=example.com 猜猜區別在哪裡?)
- 同網域之間共享 Cookie
- (待補充...)
總之我想點出的是,透過瀏覽器機制的 Cookie 來實作有時候並不是那麼方便,所以讓我想到了另外一個方法,也就是反思 2。
反思 2:如果我們盡量不用 Cookie 呢?
網路上順手查了一下,發現這篇文章(Cookies Are Bad for You),這裡頭提到的做法我覺得蠻值得參考的。
The key is to choose a mechanism that is controlled by the web application, not the browser
既然用 Cookie 我們必須仰賴瀏覽器的機制的話,那麼乾脆不要用 cookie,把整個實作轉交給 JavaScript,什麼意思呢?
- 在 JavaScript 當中可以用
fetch
裡頭的credentials: include
來決定是否發送 Cookie,還可以搭配 CORS Header - 透過 JavaScript 可以有效防止 CSRF 攻擊(下面詳述)
- 不用依賴各個瀏覽器的實作
關於避免 CSRF 攻擊的方法,我們可以要求任何需要使用者身份的請求都必須加上 Request Header,像是 Authorization header 等等,避免 CSRF 攻擊,也不用實作任何 CSRF Token 機制。
在 submit 表單的時候也不直接使用瀏覽器的機制,而是用 JavaScript 的 API 來做。
const form = new Form()
form.append('keyA', 'valueA');
fetch('/my-api', { body: form, method: 'POST', headers: { 'Authorization': 'xxx' } })
不過利用 JavaScript 的做法,除了過期機制跟請求要自己送之外,還要考慮以下幾點:
- 資料存哪裡?
- 如果 XSS 怎麼辦?
- 使用者如果關掉 JavaScript 怎麼辦?
資料存哪裡?
資料(access token)可以放在 in memory、 localStorage
或是 sessionStorage
甚至是 indexDB
,我知道剛開始聽起來有點怪怪的,如果遇到 XSS 怎麼辦?我們繼續看下去。
首先就是不要在客戶端放太多敏感資訊,access token 的時間也盡量設短一點,確保 token 洩漏時影響不要擴大,再透過 refresh token 的機制確保使用者體驗。
XSS 怎麼辦?
我會說任何的機制都有一定的風險,就算是 Cookie 也曾經被爆出過安全漏洞,在前端框架的幫助下,或許已經可以避開大部分的 XSS 漏洞也說不定。
使用者如果關掉 JavaScript 怎麼辦?
關掉 JavaScript 我覺得是個取捨,看看 facebook, youtube, netflix,這些服務不都要求你要開啟 JavaScript?
關於 Screen Reader 的部分,我覺得在之後或許會有更多 screen reader 會加入基本的 JavaScript 來獲得更好的體驗,雖然最近的趨勢有點讓 JavaScript 動輒幾百 KB 一大包,不過不管在 screen reader 或是 accessibility 上,用 JavaScript 可以提供更多細膩的操作。
那麼這樣一來就連 img, form 之類的是不是也要用 javascript 打 API?我會說在 SPA 當道的時代,已經越來越多服務都是用 XHR 做 submit API,除了必須要用 JavaScript 跟還要寫扣處理麻煩一點之外,換來更可靠的安全性。
反思 3: OAuth
這是在文章中提到的。
透過 OAuth 的協定,我們可以跟 authorization server 去交換 token,將 token 儲存在客戶端供使用,然後再透過 HMAC 的演算法確保 request method 跟 request URL 是正確的。
結論
我自己覺得安全性跟方便性本來就是一體兩面的事情,雖然把所有的實作、驗證、過期等機制都搬到服務端來做有點麻煩,以前我也覺得 Cookie 很安全很好用,到現在看到這個政策改變,再加上之前看到大大小小的案例,讓我重新思考了一遍,或許這篇文章裡頭還有當初沒有思考到的觀點,非常歡迎指教。