National Central University
Uedu Main Site
Explore Uedu
Student Console
Register as Member/Login
Research Informed Consent Center
Survey Center
Educational Big Data Challenge
Claim or create a course
Try an AI Scenario Task
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

3

Local AI

--

Uedu Code

--

中央大學 AQI 78 24°C PM2.5 18
AI Reply Desktop Notifications

Show a desktop notification when the AI TA finishes replying

Chat Message Notifications

Notify me when classmates post messages in the forum

Sound notification

Play an alert sound whenever there is a new notification

Unit 09

跨站請求偽造(CSRF)

跨站請求的條件與防護權杖
Interaction 30 minute(s)

Learning Objectives

  • 說明瀏覽器自動帶上 Cookie 如何造成跨站請求偽造,以及攻擊成立的條件
  • 以防護權杖修補表單,並確認攻擊失效、正常表單仍可使用
  • 說明 SameSite 的效果與限制,以及為何不應以 GET 改變狀態
  • 區分 CSRF 與 XSS,並說明 CORS 為何不是 CSRF 防護

瀏覽器自動帶上 Cookie 的後果

學生登入校園影印額度網站之後,在同一個瀏覽器開啟另一個網站。那個網站的頁面含有一個表單,目標是影印額度網站的轉出功能。學生按下按鈕時,瀏覽器會送出請求,並依照 Cookie 的規則附上影印額度網站的 Cookie。伺服器看到的是一個帶有有效工作階段的請求,無從判斷這個請求是否出於學生的本意。這類攻擊稱為跨站請求偽造(cross-site request forgery,CSRF)。

攻擊者不需要讀到 Cookie,也讀不到回應內容,只要讓請求送出就達到目的。依 MDN 的整理,網站符合下列條件時,CSRF 就可能成立:

  1. 以 HTTP 請求改變伺服器的狀態
  2. 只靠 Cookie 確認請求來自已登入的使用者
  3. 請求的參數都可以被攻擊者預測

防護的方向是破壞其中一項。最常見的作法是加入攻擊者無法預測的參數,稱為防護權杖(CSRF token)。

本課的模擬瀏覽器在未設定 SameSite 時一律送出 Cookie,相當於沒有採用 Lax 預設值的瀏覽器。部分瀏覽器以 Lax 為預設值,但不同瀏覽器的行為並不一致,網站不應該依賴預設值。

互動練習:跨站表單與防護權杖

本區塊為模擬環境。 兩個網站都只在你的瀏覽器內模擬,不連線到任何外部系統,帳號與額度皆為虛構。未經授權對真實網站進行相同操作可能觸犯法律(第 1 課)。

按「啟動伺服器」後依任務操作,「另一個網站」按鈕在網址列旁邊。程式已備妥 csrf_token() 與 valid_csrf() 兩個函式,但還沒有使用。修補需要改兩處:

<!-- templates/index.html 的轉出表單內 -->
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
# app.py 的 transfer(),放在確認登入之後
if not valid_csrf():
    abort(403)

防護權杖的設計

防護權杖能發揮作用,是因為另一個網站讀不到這個網站的頁面內容,也就無法得知權杖的值。這項限制來自瀏覽器的同源政策(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。

互動練習:SameSite 與以 GET 改變狀態

本區塊為模擬環境。 兩個網站都只在你的瀏覽器內模擬,不連線到任何外部系統,帳號與額度皆為虛構。未經授權對真實網站進行相同操作可能觸犯法律(第 1 課)。

SameSite 的設定方式如下,加在 app.secret_key 下一行。每次修改後要重新登入,瀏覽器才會收到帶有新屬性的 Cookie:

app.config['SESSION_COOKIE_SAMESITE'] = '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 設定沒有放寬。

互動練習:CORS 與跨站請求

本區塊為模擬環境。 兩個網站都只在你的瀏覽器內模擬,不連線到任何外部系統,帳號與額度皆為虛構。未經授權對真實網站進行相同操作可能觸犯法律(第 1 課)。

模擬瀏覽器會依 CORS 規則處理 fetch,結果與被擋下的原因都寫在「伺服器紀錄」分頁。第三個任務要加入下列程式,示範放寬 CORS 的後果,真實網站不應該這樣設定。攻擊頁的 fetch 帶上 Cookie,所以來源必須寫明,不能使用 *,也需要 Access-Control-Allow-Credentials;Content-Type: application/json 要列在 Access-Control-Allow-Headers:

@app.after_request
def add_cors(resp):
    resp.headers['Access-Control-Allow-Origin'] = 'https://other.example'
    resp.headers['Access-Control-Allow-Credentials'] = 'true'
    resp.headers['Access-Control-Allow-Headers'] = 'Content-Type'
    return resp

重點整理

  • 瀏覽器會自動帶上 Cookie,所以只靠 Cookie 驗證身分的請求可以由其他網站觸發
  • 防護權杖由伺服器產生,放在表單或標頭,每個改變狀態的請求都要驗證,包含登入與登出;另一個網站讀不到頁面,無法取得權杖
  • 不應以 GET 執行狀態變更,這類請求不受 SameSite=Lax 保護
  • SameSite 是縱深防禦,有效降低風險,但不能取代防護權杖;應明確設定,不要依賴瀏覽器預設值
  • CORS 不是 CSRF 防護:簡單請求不經預檢就會送出,放寬 CORS 反而可能造成缺口
  • XSS 可以突破 CSRF 防護,兩者要分別修補
  • 只在自己擁有或獲得明確授權的系統上進行測試

下一課討論存取控制:通過認證的使用者,能做的事情應該如何限制。

小測驗

1.

CSRF 攻擊中,攻擊者通常做不到下列哪一件事?

2.

為什麼防護權杖要放在表單隱藏欄位,而不能只放在 Cookie?

3.

網站把工作階段 Cookie 設為 SameSite=Lax,而且有一個以 GET 轉出額度的連結。下列敘述何者正確?

4.

某 API 接受表單格式的請求,而且沒有設定任何 CORS 標頭。跨站的表單格式 fetch 會發生什麼事?

5.

網站存在 XSS 漏洞,也有正確實作的防護權杖。下列敘述何者正確?

6.

比較 SameSite=Strict 與 Lax,下列何者正確?

第 1 次作答
這一課完成了!
Next lesson:存取控制
你目前沒有登入,學習進度只存在這個瀏覽器。 Login 後,進度會跟著帳號,換電腦也看得到。