Learning Objectives
- 說明瀏覽器自動帶上 Cookie 如何造成跨站請求偽造,以及攻擊成立的條件
- 以防護權杖修補表單,並確認攻擊失效、正常表單仍可使用
- 說明 SameSite 的效果與限制,以及為何不應以 GET 改變狀態
- 區分 CSRF 與 XSS,並說明 CORS 為何不是 CSRF 防護
瀏覽器自動帶上 Cookie 的後果
學生登入校園影印額度網站之後,在同一個瀏覽器開啟另一個網站。那個網站的頁面含有一個表單,目標是影印額度網站的轉出功能。學生按下按鈕時,瀏覽器會送出請求,並依照 Cookie 的規則附上影印額度網站的 Cookie。伺服器看到的是一個帶有有效工作階段的請求,無從判斷這個請求是否出於學生的本意。這類攻擊稱為跨站請求偽造(cross-site request forgery,CSRF)。
攻擊者不需要讀到 Cookie,也讀不到回應內容,只要讓請求送出就達到目的。依 MDN 的整理,網站符合下列條件時,CSRF 就可能成立:
- 以 HTTP 請求改變伺服器的狀態
- 只靠 Cookie 確認請求來自已登入的使用者
- 請求的參數都可以被攻擊者預測
防護的方向是破壞其中一項。最常見的作法是加入攻擊者無法預測的參數,稱為防護權杖(CSRF token)。
本課的模擬瀏覽器在未設定 SameSite 時一律送出 Cookie,相當於沒有採用 Lax 預設值的瀏覽器。部分瀏覽器以 Lax 為預設值,但不同瀏覽器的行為並不一致,網站不應該依賴預設值。
防護權杖的設計
防護權杖能發揮作用,是因為另一個網站讀不到這個網站的頁面內容,也就無法得知權杖的值。這項限制來自瀏覽器的同源政策(same-origin policy),前提是網站沒有 XSS 漏洞,CORS 也沒有放寬。權杖的要求如下:
- 由伺服器以密碼學安全的亂數產生,每個工作階段至少一個,並與工作階段綁定
- 放在表單的隱藏欄位或自訂標頭,不能只放在 Cookie,因為 Cookie 會被瀏覽器自動帶上
- 伺服器端在每個改變狀態的請求都要驗證,權杖缺少或不符就拒絕
- 比對權杖時使用
secrets.compare_digest這類常數時間比較,避免比對時間透露內容
下方練習為了聚焦只修補 /transfer。真實網站的 /logout 同樣要驗證。登入表單也要防護:攻擊者可能讓受害者在不知情下登入攻擊者的帳號,這稱為 login CSRF,OWASP 建議在登入前就建立工作階段並發給權杖。
範例的 valid_csrf() 先確認工作階段中確實有權杖。如果直接比較 request.form.get('csrf_token') != session.get('csrf_token'),兩邊都沒有值時會得到「相等」,檢查形同虛設。
實務上應該使用框架提供的機制,例如 Flask-WTF 的 CSRFProtect,它預設會檢查所有 POST、PUT、PATCH、DELETE 請求,不需要逐一呼叫驗證函式,但表單仍要放入權杖。以 GET 改變狀態的路由不在檢查範圍內。
其他防護方式包括檢查 Origin 標頭與 Sec-Fetch-Site 標頭。OWASP 提醒,缺少標頭或 Origin: null 不能視為同源請求。
XSS 可以突破所有 CSRF 防護。 注入的腳本在同一個來源執行,能夠讀到頁面上的權杖並送出請求(第 8 課)。兩種漏洞的差別如下:
| CSRF | XSS | |
|---|---|---|
| 攻擊者的程式在哪裡執行 | 另一個網站 | 目標網站的頁面內 |
| 能否讀到目標網站的回應 | 不能 | 可以 |
| 主要防護 | 防護權杖 | 輸出轉義 |
SameSite 的效果與限制
SameSite 屬性限制瀏覽器在跨站請求中是否帶上 Cookie:
| 值 | 跨站請求的行為 |
|---|---|
Strict |
一律不帶 Cookie,包含從其他網站點連結進來 |
Lax |
只有頂層導覽且使用安全方法(如 GET)時才帶 Cookie;跨站 POST、fetch、iframe、圖片都不帶 |
None |
一律帶 Cookie,且必須同時設定 Secure |
MDN 與 OWASP 都將 SameSite 列為縱深防禦,不能取代防護權杖,原因如下:
- 以 GET 改變狀態的功能不受
Lax保護。 跨站連結是頂層 GET,Cookie 會被帶上。所以不應該以 GET 執行狀態變更,下方練習會實際觀察這一點。 - 方法覆寫。 部分框架允許以 GET 請求模擬 POST,同樣會失去保護。
- 「同站」不等於「同來源」。
foo.campus.example與bar.campus.example屬於同站,依賴SameSite就要信任所有子網域。 - 未明確設定時的行為不一致。 採用
Lax預設值的瀏覽器,對剛設定不到兩分鐘的 Cookie 還可能在跨站 POST 時送出。
Strict 的保護較強,代價是從其他網站點連結進來時會顯示為尚未登入。MDN 的建議是:用於改變狀態、不允許跨站發出的請求的 Cookie 使用 Strict;用來決定是否讓已登入的使用者看到某個頁面的 Cookie 使用 Lax。
CORS 與 CSRF 的關係
跨來源資源共用(cross-origin resource sharing,CORS)規範其他來源的頁面能否讀取回應,以及哪些請求要先經過預檢。常見的誤解是「沒有開放 CORS 就不會有 CSRF」。實際上 CORS 不是 CSRF 的防護機制,理由如下:
- 表單格式的請求屬於簡單請求,不需要預檢就會送出。瀏覽器只是不讓對方的頁面讀取回應,伺服器已經處理完成
- 一般的
<form>送出完全不經過 CORS 檢查 - CORS 是放寬同源政策的機制,設定不當反而會打開缺口。MDN 指出,如果回應包含允許該來源的
Access-Control-Allow-Origin與Access-Control-Allow-Credentials,該來源就可能發動 CSRF
只接受 application/json 的 API 比較不易受到影響,因為跨站送出 JSON 需要預檢,而預檢沒有通過時請求不會送出。這項效果的前提是伺服器確實拒絕其他內容類型,而且 CORS 設定沒有放寬。
重點整理
- 瀏覽器會自動帶上 Cookie,所以只靠 Cookie 驗證身分的請求可以由其他網站觸發
- 防護權杖由伺服器產生,放在表單或標頭,每個改變狀態的請求都要驗證,包含登入與登出;另一個網站讀不到頁面,無法取得權杖
- 不應以 GET 執行狀態變更,這類請求不受
SameSite=Lax保護 SameSite是縱深防禦,有效降低風險,但不能取代防護權杖;應明確設定,不要依賴瀏覽器預設值- CORS 不是 CSRF 防護:簡單請求不經預檢就會送出,放寬 CORS 反而可能造成缺口
- XSS 可以突破 CSRF 防護,兩者要分別修補
- 只在自己擁有或獲得明確授權的系統上進行測試
下一課討論存取控制:通過認證的使用者,能做的事情應該如何限制。
小測驗
CSRF 攻擊中,攻擊者通常做不到下列哪一件事?
為什麼防護權杖要放在表單隱藏欄位,而不能只放在 Cookie?
網站把工作階段 Cookie 設為 SameSite=Lax,而且有一個以 GET 轉出額度的連結。下列敘述何者正確?
某 API 接受表單格式的請求,而且沒有設定任何 CORS 標頭。跨站的表單格式 fetch 會發生什麼事?
網站存在 XSS 漏洞,也有正確實作的防護權杖。下列敘述何者正確?
比較 SameSite=Strict 與 Lax,下列何者正確?