跨站請求偽造 (CSRF)

在跨站請求偽造 (CSRF) 攻擊中,攻擊者會欺騙使用者或瀏覽器,使其從惡意網站向目標網站發起一個 HTTP 請求。該請求包含使用者的憑據,並導致伺服器執行一些有害操作,而伺服器卻認為這是使用者本意。

概述

網站通常會代表使用者執行特殊操作——例如,購買產品或進行預約——透過接收來自使用者瀏覽器的 HTTP 請求,其中通常包含詳細說明要執行的操作的引數。為了確保請求確實來自相關使用者,伺服器期望請求包含使用者的憑據:例如,包含使用者會話 ID 的 cookie。

在下面的示例中,使用者之前已登入到其銀行,並且瀏覽器已儲存了使用者的會話 cookie。該頁面包含一個<form> 元素,允許使用者將資金轉賬給他人。當用戶提交表單時,瀏覽器會向伺服器傳送一個POST 請求,其中包含表單資料。如果使用者已登入,則請求將包含使用者的 cookie。伺服器會驗證 cookie 並執行特殊操作——在這種情況下,是轉賬。

Diagram showing a user submitting a browser form, the browser then making a POST request to the server, and the server validating the request.

在本指南中,我們將這種執行特殊操作的請求稱為“狀態更改請求”。

在 CSRF 攻擊中,攻擊者會建立一個包含表單的網站。表單的action 屬性設定為銀行的網站,並且表單包含模仿銀行欄位的隱藏輸入欄位。

html
<form action="https://my-bank.example.org/transfer" method="POST">
  <input type="hidden" name="recipient" value="attacker" />
  <input type="hidden" name="amount" value="1000" />
</form>

頁面還包含在頁面載入時提交表單的 JavaScript。

js
const form = document.querySelector("form");
form.submit();

當用戶訪問該頁面時,瀏覽器會將表單提交到銀行的網站。由於使用者已登入到其銀行,因此請求可能會包含使用者的真實 cookie,從而銀行伺服器成功驗證請求並轉賬。

Diagram showing a CSRF attack in which a decoy page submits a POST request to the website for the user's bank.

攻擊者還可以透過其他方式發起跨站請求偽造。例如,如果網站使用GET 請求來執行操作,那麼攻擊者就可以完全避免使用表單,並透過向用戶傳送一個包含如下標記的頁面的連結來執行攻擊:

html
<img
  src="https://my-bank.example.org/transfer?recipient=attacker&amount=1000" />

當用戶載入頁面時,瀏覽器會嘗試獲取影像資源,而這實際上是事務請求。

通常,如果您的網站執行以下操作,則可能發生 CSRF 攻擊:

  • 使用 HTTP 請求更改伺服器上的某些狀態。
  • 僅使用 cookie 來驗證請求是否來自已認證使用者。
  • 僅使用攻擊者可以預測的請求中的引數。

防範 CSRF 的方法

在本節中,我們將概述三種替代的 CSRF 防禦方法以及一種可用於為其中任何一種提供縱深防禦的第四種實踐。

  • 第一個主要防禦措施是使用嵌入在頁面中的CSRF 令牌。這是最常見的方法,如果您是從表單元素髮出狀態更改請求,就像我們上面的示例一樣。

  • 第二個是使用Fetch metadata HTTP 標頭來檢查狀態更改請求是否是跨站發起的。

  • 第三個是確保狀態更改請求不是簡單請求,這樣跨源請求預設就會被阻止。此方法適用於您透過 JavaScript API(如fetch())發出狀態更改請求的情況。

最後,我們將討論SameSite cookie 屬性,它可以用於為前述任何一種方法提供縱深防禦。

CSRF 令牌

在此防禦機制中,當伺服器提供頁面時,它會在頁面中嵌入一個不可預測的值,稱為 CSRF 令牌。然後,當合法頁面將狀態更改請求傳送到伺服器時,它會在 HTTP 請求中包含 CSRF 令牌。伺服器隨後可以檢查令牌值,僅在匹配時才執行請求。由於攻擊者無法猜測令牌值,因此他們無法成功發起偽造。即使攻擊者在令牌被使用後發現它,如果令牌每次都發生變化,請求也無法重放。

對於表單提交,CSRF 令牌通常包含在一個隱藏的表單欄位中,以便在表單提交時自動傳送回伺服器進行檢查。

對於像 fetch() 這樣的 JavaScript API,令牌可能會放在 cookie 中或嵌入在頁面中,然後 JavaScript 會提取該值並將其作為額外的標頭髮送。

現代 Web 框架通常內建對 CSRF 令牌的支援:例如,Django 允許您使用csrf_token 標籤來保護表單。這會生成一個包含令牌的額外隱藏表單欄位,然後在伺服器上由框架進行檢查。

要利用此保護,您必須瞭解您網站中所有使用狀態更改 HTTP 請求的地方,並確保您正在使用所選框架提供的保護。

Fetch metadata

Fetch metadata 是一組由瀏覽器新增的 HTTP 請求標頭,它們提供有關 HTTP 請求上下文的額外資訊。伺服器可以使用這些標頭來決定是否允許請求。

與 CSRF 最相關的是Sec-Fetch-Site 標頭,它告訴伺服器該請求是同源、同站、跨站還是使用者直接發起的。伺服器可以使用此資訊允許跨源請求,或阻止它們作為潛在的 CSRF 攻擊。

例如,這段Express程式碼只允許同站和同源請求。

js
app.post("/transfer", (req, res) => {
  const secFetchSite = req.headers["sec-fetch-site"];
  if (secFetchSite === "same-origin" || secFetchSite === "same-site") {
    console.log("allowed");
    // Update state
  } else {
    console.log("denied");
    // Don't update state
  }
});

有關 Fetch metadata 標頭的完整列表,請參閱Fetch metadata 請求標頭,有關使用此功能的指南,請參閱使用 Fetch Metadata 保護您的資源免受 Web 攻擊

避免簡單請求

Web 瀏覽器區分兩種 HTTP 請求:簡單請求和其他請求。

簡單請求(這是由 <form> 元素提交引起的請求型別)可以在不被阻止的情況下跨源發出。由於 Web 早期以來表單就能發起跨源請求,因此出於相容性考慮,它們仍應能夠發起跨源請求。這就是為什麼我們需要實現其他策略來防禦表單免受 CSRF 攻擊,例如使用 CSRF 令牌。

然而,Web 平臺的其他部分,特別是像 fetch() 這樣的 JavaScript API,可以發出不同型別的請求(例如,設定自定義標頭的請求),而這些請求預設情況下不允許跨源,因此 CSRF 攻擊不會成功。

因此,使用 fetch()XMLHttpRequest 的網站可以透過確保其發出的狀態更改請求永遠不是簡單請求來防禦 CSRF。

例如,將請求的 Content-Type 設定為 "application/json" 將阻止它被視為簡單請求。

js
fetch("https://my-bank.example.org/transfer", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ recipient: "joe", amount: "100" }),
});

同樣,在請求中設定自定義標頭也將阻止它被視為簡單請求。

js
fetch("https://my-bank.example.org/transfer", {
  method: "POST",
  headers: {
    "X-MY-BANK-ANTI-CSRF": 1,
  },
  body: JSON.stringify({ recipient: "joe", amount: "100" }),
});

標頭名稱可以是任何名稱,只要它不與標準標頭衝突即可。

伺服器隨後可以檢查該標頭是否存在:如果存在,則伺服器知道該請求未被視為簡單請求。

非簡單請求和 CORS

我們已經說過,非簡單請求預設情況下不會跨源傳送。關鍵在於 跨源資源共享 (CORS) 協議允許網站放寬此限制。

具體來說,如果您的網站對狀態更改請求的響應包含以下內容,那麼它將容易受到來自特定來源的 CSRF 攻擊:

  • Access-Control-Allow-Origin 響應標頭,並且該標頭列出了傳送方的來源。
  • Access-Control-Allow-Credentials 響應標頭。

縱深防禦:SameSite cookie

SameSite cookie 屬性提供了一些針對 CSRF 攻擊的保護。它不是一個完整的防禦措施,最好將其視為其他防禦措施的補充,提供一定程度的縱深防禦。

此屬性控制瀏覽器何時允許在跨站請求中包含 cookie。它有三個可能的值:NoneLaxStrict

Strict 值提供最強的保護:如果設定了此屬性,瀏覽器將不會在任何跨站請求中包含 cookie。然而,這會帶來可用性問題:如果使用者登入到您的網站,然後從另一個網站連結到您的網站,那麼您的 cookie 將不會被包含,並且使用者到達您的網站時將不被識別。

Lax 值放寬了此限制:如果滿足以下兩個條件,則 cookie 會包含在跨站請求中:

  • 請求是頂級瀏覽上下文的導航。
  • 請求使用了安全的方法:特別是,GET 是安全的,但POST 不是。

然而,Lax 提供的保護比 Strict 弱得多。

  • 攻擊者可以觸發頂級導航。例如,在本文開頭,我們展示了一個 CSRF 攻擊,其中攻擊者將表單提交給目標:這被視為頂級導航。如果表單使用 GET 提交,則請求仍會包含帶有 SameSite=Lax 的 cookie。
  • 即使伺服器確實檢查了請求是否未使用 GET 傳送,某些 Web 框架也支援“方法覆蓋”:這允許攻擊者使用 GET 傳送請求,但讓伺服器認為它使用了 POST

總的來說,您應該嘗試對某些 cookie 使用 Strict,對其他 cookie 使用 Lax

  • 對用於確定已登入使用者是否應顯示頁面的 cookie 使用 Lax
  • 對用於狀態更改請求且您不想允許跨站訪問的 cookie 使用 Strict

SameSite 屬性的另一個問題是它保護您免受來自不同站點的請求,而不是不同來源的請求。這是一種更寬鬆的保護,因為(例如)https://foo.example.orghttps://bar.example.org 被視為同一個站點,儘管它們是不同的來源。實際上,如果您依賴同站保護,則必須信任您站點中的所有子域名。

有關 SameSite 限制的更多詳細資訊,請參閱繞過 SameSite cookie 限制

防禦總結清單

  • 瞭解您網站中實現狀態更改請求的位置,這些請求使用會話 cookie 來檢查是哪個使用者發出了請求。
  • 實施本文件中描述的至少一種主要防禦措施。
    • 如果您使用 <form> 元素髮出這些請求,請確保您使用的是支援 CSRF 令牌的 Web 框架,並加以使用。
    • 如果您使用 fetch()XMLHttpRequest 等 JavaScript API 發出狀態更改請求,請確保它們不是簡單請求。
    • 無論您使用哪種機制發出請求,都請考慮使用 Fetch metadata 來禁止跨站請求。
  • 避免使用 GET 方法發出狀態更改請求。
  • 如果可能,將會話 cookie 的 SameSite 屬性設定為 Strict,如果必須,則設定為 Lax

另見