跨站請求偽造 (CSRF)
Spring 為抵禦 跨站請求偽造 (CSRF) 攻擊提供了全面的支援。在以下章節中,我們將探討
什麼是 CSRF 攻擊?
理解 CSRF 攻擊的最好方法是看一個具體的例子。
假設你的銀行網站提供一個表單,允許將資金從當前登入使用者轉移到另一個銀行賬戶。例如,轉賬表單可能看起來像這樣:
<form method="post"
action="/transfer">
<input type="text"
name="amount"/>
<input type="text"
name="routingNumber"/>
<input type="text"
name="account"/>
<input type="submit"
value="Transfer"/>
</form>
對應的 HTTP 請求可能看起來像這樣:
POST /transfer HTTP/1.1
Host: bank.example.com
Cookie: JSESSIONID=randomid
Content-Type: application/x-www-form-urlencoded
amount=100.00&routingNumber=1234&account=9876
現在假設你登入了你的銀行網站,然後,在沒有登出的情況下,訪問了一個惡意網站。該惡意網站包含一個帶有以下表單的 HTML 頁面:
<form method="post"
action="https://bank.example.com/transfer">
<input type="hidden"
name="amount"
value="100.00"/>
<input type="hidden"
name="routingNumber"
value="evilsRoutingNumber"/>
<input type="hidden"
name="account"
value="evilsAccountNumber"/>
<input type="submit"
value="Win Money!"/>
</form>
你喜歡贏錢,所以你點選了提交按鈕。在這個過程中,你無意中向惡意使用者轉移了 100 美元。這是因為,雖然惡意網站無法看到你的 Cookie,但與你的銀行相關的 Cookie 仍然隨請求一起傳送。
更糟糕的是,整個過程可以透過 JavaScript 自動化。這意味著你甚至不需要點選按鈕。此外,當你訪問一個受到 XSS 攻擊 的誠實網站時,這也很容易發生。那麼我們如何保護使用者免受此類攻擊呢?
應對 CSRF 攻擊的防護
CSRF 攻擊之所以可能發生,是因為受害者網站傳送的 HTTP 請求與攻擊者網站傳送的請求完全相同。這意味著沒有辦法拒絕來自惡意網站的請求,而只允許來自銀行網站的請求。為了防止 CSRF 攻擊,我們需要確保請求中包含一些惡意網站無法提供的東西,以便我們可以區分這兩個請求。
Spring 提供了兩種機制來防止 CSRF 攻擊:
-
在你的會話 Cookie 上指定 SameSite 屬性
這兩種防護都需要 安全方法是隻讀的。 |
安全方法必須是隻讀的
為了使 任一種防護 起作用,應用程式必須確保 “安全”的 HTTP 方法是隻讀的。這意味著使用 HTTP GET
、HEAD
、OPTIONS
和 TRACE
方法的請求不應該改變應用程式的狀態。
Synchronizer Token Pattern
防止 CSRF 攻擊的主要且最全面的方法是使用 Synchronizer Token Pattern。此解決方案是確保每個 HTTP 請求除了我們的會話 Cookie 外,還需要在 HTTP 請求中包含一個安全的隨機生成值,稱為 CSRF 令牌。
提交 HTTP 請求時,伺服器必須查詢預期的 CSRF 令牌,並將其與 HTTP 請求中的實際 CSRF 令牌進行比較。如果值不匹配,則應拒絕該 HTTP 請求。
這種方法奏效的關鍵在於實際的 CSRF 令牌應位於 HTTP 請求中瀏覽器不會自動包含的部分。例如,要求在 HTTP 引數或 HTTP 頭中包含實際的 CSRF 令牌將防止 CSRF 攻擊。要求在 Cookie 中包含實際的 CSRF 令牌不起作用,因為 Cookie 會由瀏覽器自動包含在 HTTP 請求中。
我們可以放寬要求,僅對每個更新應用程式狀態的 HTTP 請求強制要求實際的 CSRF 令牌。為此,我們的應用程式必須確保 安全 HTTP 方法是隻讀的。這提高了可用性,因為我們希望允許從外部網站連結到我們的網站。此外,我們不希望在 HTTP GET 請求中包含隨機令牌,因為這可能導致令牌洩露。
考慮一下當使用 Synchronizer Token Pattern 時, 我們的示例 會如何改變。假設實際的 CSRF 令牌必須位於名為 _csrf
的 HTTP 引數中。我們的應用程式的轉賬表單將如下所示:
<form method="post"
action="/transfer">
<input type="hidden"
name="_csrf"
value="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/>
<input type="text"
name="amount"/>
<input type="text"
name="routingNumber"/>
<input type="hidden"
name="account"/>
<input type="submit"
value="Transfer"/>
</form>
表單現在包含一個帶有 CSRF 令牌值的隱藏輸入欄位。外部網站無法讀取 CSRF 令牌,因為同源策略確保惡意網站無法讀取響應。
對應的轉賬 HTTP 請求將如下所示:
POST /transfer HTTP/1.1
Host: bank.example.com
Cookie: JSESSIONID=randomid
Content-Type: application/x-www-form-urlencoded
amount=100.00&routingNumber=1234&account=9876&_csrf=4bfd1575-3ad1-4d21-96c7-4ef2d9f86721
你會注意到 HTTP 請求現在包含帶有安全隨機值的 _csrf
引數。惡意網站將無法提供正確的 _csrf
引數值(必須在惡意網站上明確提供),當伺服器比較實際 CSRF 令牌和預期 CSRF 令牌時,轉賬將失敗。
SameSite 屬性
一種新的防止 CSRF 攻擊 的方法是在 Cookie 上指定 SameSite 屬性。伺服器在設定 Cookie 時可以指定 SameSite
屬性,以指示當請求來自外部站點時,不應傳送該 Cookie。
Spring Security 不直接控制會話 Cookie 的建立,因此它不直接支援 SameSite 屬性。Spring Session 為基於 Servlet 的應用程式提供了 |
例如,帶有 SameSite
屬性的 HTTP 響應頭可能看起來像這樣:
Set-Cookie: JSESSIONID=randomid; Domain=bank.example.com; Secure; HttpOnly; SameSite=Lax
SameSite
屬性的有效值有:
考慮一下 我們的示例 如何使用 SameSite
屬性進行保護。銀行應用程式可以透過在會話 Cookie 上指定 SameSite
屬性來防止 CSRF 攻擊。
在會話 Cookie 上設定 SameSite
屬性後,瀏覽器會繼續向來自銀行網站的請求傳送 JSESSIONID
Cookie。然而,瀏覽器將不再向來自惡意網站的轉賬請求傳送 JSESSIONID
Cookie。由於會話不再存在於來自惡意網站的轉賬請求中,應用程式將受到 CSRF 攻擊的保護。
使用 SameSite
屬性防止 CSRF 攻擊時,需要注意一些重要的 考慮事項。
將 SameSite
屬性設定為 Strict
提供了更強的防禦能力,但也可能讓使用者感到困惑。考慮一個一直登入社交媒體網站 social.example.com 的使用者。該使用者在 email.example.org 收到一封包含社交媒體網站連結的電子郵件。如果使用者點選該連結,他們理應期望能直接登入社交媒體網站。然而,如果 SameSite
屬性設定為 Strict
,Cookie 將不會被髮送,因此使用者將無法登入。
另一個需要注意的顯而易見的因素是,為了使 SameSite
屬性保護使用者,瀏覽器必須支援 SameSite
屬性。大多數現代瀏覽器都 支援 SameSite 屬性。然而,仍在使用的舊瀏覽器可能不支援。
基於這個原因,我們通常建議將 SameSite
屬性作為一種深度防禦手段,而不是唯一的 CSRF 攻擊防護措施。
何時使用 CSRF 防護
你應該何時使用 CSRF 防護?我們的建議是,對於任何可能由普通使用者透過瀏覽器處理的請求,都應使用 CSRF 防護。如果你正在建立一個只供非瀏覽器客戶端使用的服務,你可能希望停用 CSRF 防護。
CSRF 防護和 JSON
一個常見的問題是:“我需要保護 JavaScript 發起的 JSON 請求嗎?” 簡短的答案是:取決於具體情況。但是,你必須非常小心,因為存在可能影響 JSON 請求的 CSRF 攻擊。例如,惡意使用者可以使用 以下表單建立一個帶有 JSON 的 CSRF 攻擊:
<form action="https://bank.example.com/transfer" method="post" enctype="text/plain">
<input name='{"amount":100,"routingNumber":"evilsRoutingNumber","account":"evilsAccountNumber", "ignore_me":"' value='test"}' type='hidden'>
<input type="submit"
value="Win Money!"/>
</form>
這將產生以下 JSON 結構:
{ "amount": 100,
"routingNumber": "evilsRoutingNumber",
"account": "evilsAccountNumber",
"ignore_me": "=test"
}
如果應用程式不驗證 Content-Type
頭,就會暴露在這種攻擊之下。根據設定的不同,一個驗證 Content-Type
的 Spring MVC 應用仍然可能透過將 URL 字尾更新為 .json
來被利用,如下所示:
<form action="https://bank.example.com/transfer.json" method="post" enctype="text/plain">
<input name='{"amount":100,"routingNumber":"evilsRoutingNumber","account":"evilsAccountNumber", "ignore_me":"' value='test"}' type='hidden'>
<input type="submit"
value="Win Money!"/>
</form>
CSRF 與無狀態瀏覽器應用
如果我的應用程式是無狀態的怎麼辦?這並不一定意味著你受到了保護。事實上,如果使用者不需要在 Web 瀏覽器中為某個特定請求執行任何操作,他們很可能仍然容易受到 CSRF 攻擊。
例如,考慮一個使用自定義 Cookie 來儲存所有狀態以進行認證的應用程式(而不是 JSESSIONID)。當發生 CSRF 攻擊時,自定義 Cookie 會像 我們之前示例 中傳送 JSESSIONID Cookie 的方式一樣隨請求一起傳送。此應用程式容易受到 CSRF 攻擊。
使用 basic 認證的應用程式也容易受到 CSRF 攻擊。應用程式之所以脆弱,是因為瀏覽器會自動在任何請求中包含使用者名稱和密碼,就像 我們之前示例 中傳送 JSESSIONID Cookie 的方式一樣。
CSRF 考慮事項
在實現 CSRF 攻擊防護時,有一些特殊考慮事項需要考慮。
登入
為了防止 偽造登入請求,登入 HTTP 請求應該受到 CSRF 攻擊的保護。保護偽造登入請求是必要的,這樣惡意使用者就無法讀取受害者的敏感資訊。攻擊過程如下:
-
惡意使用者使用其憑據執行 CSRF 登入。受害者現在以惡意使用者的身份進行認證。
-
然後,惡意使用者欺騙受害者訪問被入侵的網站並輸入敏感資訊。
-
資訊與惡意使用者的帳戶關聯,因此惡意使用者可以使用自己的憑據登入並檢視受害者的敏感資訊。
確保登入 HTTP 請求受到 CSRF 攻擊保護的一個可能的複雜問題是,使用者可能會遇到會話超時,導致請求被拒絕。會話超時對於不期望登入需要會話的使用者來說是令人驚訝的。更多資訊請參考 CSRF 與會話超時。
登出
為了防止偽造登出請求,登出 HTTP 請求應該受到 CSRF 攻擊的保護。保護偽造登出請求是必要的,這樣惡意使用者就無法讀取受害者的敏感資訊。有關攻擊詳情,請參閱 這篇部落格文章。
確保登出 HTTP 請求受到 CSRF 攻擊保護的一個可能的複雜問題是,使用者可能會遇到會話超時,導致請求被拒絕。會話超時對於不期望登出需要會話的使用者來說是令人驚訝的。更多資訊請參閱 CSRF 與會話超時。
CSRF 與會話超時
通常情況下,預期的 CSRF 令牌儲存在會話中。這意味著,一旦會話過期,伺服器將找不到預期的 CSRF 令牌並拒絕該 HTTP 請求。有許多選項(每種都有權衡)可以解決超時問題:
-
解決超時問題的最佳方法是使用 JavaScript 在表單提交時請求 CSRF 令牌。然後使用 CSRF 令牌更新表單並提交。
-
另一種選擇是使用一些 JavaScript 告訴使用者他們的會話即將過期。使用者可以點選一個按鈕來繼續並重新整理會話。
-
最後,預期的 CSRF 令牌可以儲存在 Cookie 中。這使得預期的 CSRF 令牌可以比會話更長時間存在。
有人可能會問,為什麼預期的 CSRF 令牌預設不儲存在 Cookie 中。這是因為存在已知的攻擊,其中可以透過另一個域設定頭部(例如,指定 Cookie)。這就是 Ruby on Rails 不再在存在頭部 X-Requested-With 時跳過 CSRF 檢查 的原因。有關如何執行此攻擊的詳細資訊,請參閱 這個 webappsec.org 帖子。另一個缺點是,透過移除狀態(即超時),你失去了在令牌被洩露時強制使其失效的能力。
Multipart (檔案上傳)
保護 multipart 請求(檔案上傳)免受 CSRF 攻擊會導致一個 先有雞還是先有蛋 的問題。為了防止發生 CSRF 攻擊,必須讀取 HTTP 請求的主體以獲取實際的 CSRF 令牌。然而,讀取主體意味著檔案已經被上傳,這意味著外部網站可以上傳檔案。
將 Spring Security 的 CSRF 防護與 multipart/form-data 檔案上傳結合使用有兩種選擇:
每種選擇都有其權衡。
在將 Spring Security 的 CSRF 防護與 multipart 檔案上傳整合之前,你應該首先確保在沒有 CSRF 防護的情況下可以進行上傳。有關使用 Spring 的 multipart 表單的更多資訊,請參閱 Spring 參考手冊的 1.1.11. Multipart Resolver 章節和 |
將 CSRF 令牌放在主體中
第一個選項是將實際的 CSRF 令牌包含在請求主體中。透過將 CSRF 令牌放在主體中,主體在執行授權之前被讀取。這意味著任何人都可以將臨時檔案放在你的伺服器上。但是,只有授權使用者才能提交由你的應用程式處理的檔案。一般來說,這是推薦的方法,因為臨時檔案上傳對大多數伺服器的影響可以忽略不計。
將 CSRF 令牌包含在 URL 中
如果不允許未經授權的使用者上傳臨時檔案是不可接受的,另一種選擇是將預期的 CSRF 令牌作為查詢引數包含在表單的 action 屬性中。這種方法的缺點是查詢引數可能會洩露。更普遍而言,將敏感資料放在主體或頭部以確保其不被洩露被認為是最佳實踐。你可以在 RFC 2616 Section 15.1.3 Encoding Sensitive Information in URI’s 中找到更多資訊。
HiddenHttpMethodFilter
一些應用程式可以使用表單引數來覆蓋 HTTP 方法。例如,以下表單可以將 HTTP 方法視為 delete
而不是 post
。
<form action="/process"
method="post">
<!-- ... -->
<input type="hidden"
name="_method"
value="delete"/>
</form>
覆蓋 HTTP 方法發生在過濾器中。該過濾器必須放在 Spring Security 的支援之前。請注意,覆蓋只發生在 post
請求上,所以這實際上不太可能引起任何真正的問題。但是,確保它放在 Spring Security 的過濾器之前仍然是最佳實踐。