跨站請求偽造 (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 攻擊
-
同步器令牌模式 同步器令牌模式
-
在您的 session Cookie 上指定 SameSite 屬性
兩種保護措施都需要 安全方法為唯讀。 |
安全方法必須為唯讀
為了使 任何一種保護措施 對抗 CSRF 有效,應用程式必須確保 「安全」HTTP 方法是唯讀的。這表示使用 HTTP GET
、HEAD
、OPTIONS
和 TRACE
方法的請求不應變更應用程式的狀態。
同步器令牌模式
防止 CSRF 攻擊的主要且最全面的方法是使用 同步器令牌模式。此解決方案是確保每個 HTTP 請求除了我們的 session Cookie 之外,還需要一個安全隨機產生的值,稱為 CSRF 令牌,存在於 HTTP 請求中。
當提交 HTTP 請求時,伺服器必須查找預期的 CSRF 令牌,並將其與 HTTP 請求中的實際 CSRF 令牌進行比較。如果值不匹配,則應拒絕 HTTP 請求。
這項工作的關鍵在於,實際的 CSRF 令牌應位於 HTTP 請求中瀏覽器不會自動包含的部分。例如,要求實際的 CSRF 令牌在 HTTP 參數或 HTTP 標頭中將防止 CSRF 攻擊。要求實際的 CSRF 令牌在 Cookie 中不起作用,因為 Cookie 會被瀏覽器自動包含在 HTTP 請求中。
我們可以放寬預期,僅對每個更新應用程式狀態的 HTTP 請求要求實際的 CSRF 令牌。為了使其運作,我們的應用程式必須確保 安全 HTTP 方法是唯讀的。這提高了可用性,因為我們希望允許從外部網站連結到我們的網站。此外,我們不希望在 HTTP GET 中包含隨機令牌,因為這可能會導致令牌洩漏。
考慮當我們使用同步器令牌模式時,我們的範例 將如何變更。假設實際的 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 不直接控制 session Cookie 的建立,因此不提供對 SameSite 屬性的支援。Spring Session 在基於 servlet 的應用程式中提供對 |
具有 SameSite
屬性的 HTTP 回應標頭範例可能如下所示
Set-Cookie: JSESSIONID=randomid; Domain=bank.example.com; Secure; HttpOnly; SameSite=Lax
SameSite
屬性的有效值為
考慮如何使用 SameSite
屬性來保護 我們的範例。銀行應用程式可以透過在 session Cookie 上指定 SameSite
屬性來防止 CSRF。
在 session Cookie 上設定 SameSite
屬性後,瀏覽器會繼續將 JSESSIONID
Cookie 與來自銀行網站的請求一起傳送。但是,瀏覽器不再將 JSESSIONID
Cookie 與來自惡意網站的轉帳請求一起傳送。由於來自惡意網站的轉帳請求中不再存在 session,因此應用程式受到 CSRF 攻擊的保護。
在使用 SameSite
屬性來防止 CSRF 攻擊時,需要注意一些重要的 考量事項。
將 SameSite
屬性設定為 Strict
可提供更強大的防禦,但可能會讓使用者感到困惑。考慮一個使用者持續登入在 social.example.com 上託管的社交媒體網站。使用者在 email.example.org 收到一封電子郵件,其中包含社交媒體網站的連結。如果使用者點擊連結,他們理所當然地會期望已驗證到社交媒體網站。但是,如果 SameSite
屬性為 Strict
,則不會傳送 Cookie,因此使用者將不會被驗證。
我們可以透過實作 gh-7537 來改善 |
另一個顯而易見的考量是,為了使 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 和無狀態瀏覽器應用程式
如果我的應用程式是無狀態的怎麼辦?這不一定表示您受到保護。事實上,如果使用者不需要在網路瀏覽器中為給定的請求執行任何動作,他們仍然可能容易受到 CSRF 攻擊。
例如,考慮一個應用程式,該應用程式使用一個自訂 Cookie,其中包含用於身份驗證的所有狀態(而不是 JSESSIONID)。當進行 CSRF 攻擊時,自訂 Cookie 會以與我們先前範例中傳送 JSESSIONID Cookie 相同的方式與請求一起傳送。此應用程式容易受到 CSRF 攻擊。
使用基本身份驗證的應用程式也容易受到 CSRF 攻擊。應用程式容易受到攻擊,因為瀏覽器會以與我們先前範例中傳送 JSESSIONID Cookie 相同的方式自動將使用者名稱和密碼包含在任何請求中。
CSRF 考量
在實作防止 CSRF 攻擊的保護措施時,需要考慮一些特殊考量。
登入
為了防止 偽造登入請求,應防止登入 HTTP 請求受到 CSRF 攻擊。防止偽造登入請求是必要的,這樣惡意使用者就無法讀取受害者的敏感資訊。攻擊執行方式如下
-
惡意使用者使用惡意使用者的憑證執行 CSRF 登入。受害者現在被驗證為惡意使用者。
-
然後,惡意使用者誘騙受害者造訪受損的網站並輸入敏感資訊。
-
資訊與惡意使用者的帳戶關聯,因此惡意使用者可以使用自己的憑證登入並查看受害者的敏感資訊。
確保登入 HTTP 請求受到 CSRF 攻擊保護的一個可能的複雜情況是,使用者可能會遇到 session 超時,從而導致請求被拒絕。Session 超時對於不期望需要 session 才能登入的使用者來說是很意外的。如需更多資訊,請參閱 CSRF 和 Session 超時。
登出
為了防止偽造登出請求,應防止登出 HTTP 請求受到 CSRF 攻擊。防止偽造登出請求是必要的,這樣惡意使用者就無法讀取受害者的敏感資訊。有關攻擊的詳細資訊,請參閱 這篇部落格文章。
確保登出 HTTP 請求受到 CSRF 攻擊保護的一個可能的複雜情況是,使用者可能會遇到 session 超時,從而導致請求被拒絕。Session 超時對於不期望需要 session 才能登出的使用者來說是很意外的。如需更多資訊,請參閱 CSRF 和 Session 超時。
CSRF 和 Session 超時
通常,預期的 CSRF 令牌儲存在 session 中。這表示,一旦 session 過期,伺服器就找不到預期的 CSRF 令牌,並拒絕 HTTP 請求。有很多選項(每個選項都有取捨)可以解決超時問題
-
緩解超時的最佳方法是使用 JavaScript 在表單提交時請求 CSRF 令牌。然後使用 CSRF 令牌更新表單並提交。
-
另一個選項是讓某些 JavaScript 讓使用者知道他們的 session 即將過期。使用者可以點擊按鈕繼續並重新整理 session。
-
最後,預期的 CSRF 令牌可以儲存在 Cookie 中。這讓預期的 CSRF 令牌比 session 存活更久。
有人可能會問,為什麼預期的 CSRF 令牌預設不儲存在 Cookie 中。這是因為存在已知的漏洞,其中標頭(例如,指定 Cookie)可以由另一個網域設定。這也是 Ruby on Rails 當存在標頭 X-Requested-With 時,不再跳過 CSRF 檢查 的原因。有關如何執行漏洞的詳細資訊,請參閱 webappsec.org 討論串。另一個缺點是,透過移除狀態(即超時),您會失去在令牌洩漏時強制使其失效的能力。
Multipart(檔案上傳)
保護 multipart 請求(檔案上傳)免受 CSRF 攻擊會導致 雞生蛋還是蛋生雞 的問題。為了防止 CSRF 攻擊發生,必須讀取 HTTP 請求的 body 以取得實際的 CSRF 令牌。但是,讀取 body 表示檔案已上傳,這表示外部網站可以上傳檔案。
有兩個選項可以使用 CSRF 保護與 multipart/form-data
每個選項都有其取捨。
在整合 Spring Security 的 CSRF 保護與 multipart 檔案上傳之前,您應首先確保您可以在沒有 CSRF 保護的情況下上傳。有關將 multipart 表單與 Spring 一起使用的更多資訊,請參閱 Spring 參考文件的 1.1.11. Multipart 解析器 章節和 |
將 CSRF 令牌放在 Body 中
第一個選項是在請求的 body 中包含實際的 CSRF 令牌。透過將 CSRF 令牌放在 body 中,會在執行授權之前讀取 body。這表示任何人都可以將暫存檔案放在您的伺服器上。但是,只有授權使用者才能提交由您的應用程式處理的檔案。一般而言,這是建議的方法,因為暫存檔案上傳對大多數伺服器的影響應該可以忽略不計。
在 URL 中包含 CSRF 令牌
如果讓未經授權的使用者上傳暫存檔案是不可接受的,則另一種替代方法是在表單的 action 屬性中以查詢參數的形式包含預期的 CSRF 令牌。這種方法的缺點是查詢參數可能會洩漏。更一般而言,將敏感資料放在 body 或標頭中以確保其不會洩漏被認為是最佳實務。您可以在 RFC 2616 第 15.1.3 節 URI 中編碼敏感資訊 中找到更多資訊。
HiddenHttpMethodFilter
某些應用程式可以使用表單參數來覆寫 HTTP 方法。例如,以下表單可以將 HTTP 方法視為 delete
而不是 post
。
<form action="/process"
method="post">
<!-- ... -->
<input type="hidden"
name="_method"
value="delete"/>
</form>
覆寫 HTTP 方法發生在篩選器中。該篩選器必須放在 Spring Security 支援之前。請注意,覆寫僅在 post
上發生,因此實際上不太可能造成任何實際問題。但是,確保它放在 Spring Security 的篩選器之前仍然是最佳實務。