Spring Security 跨站請求偽造保護
1. 前言
很多小伙伴在開發(fā) Spring Security 項目時候,本地測試都沒有問題,一放到生產(chǎn)環(huán)境后,就會遇到「Invalid CSRF Token」問題,這其實是 Spring Security 防止服務(wù)免受「跨站請求偽造」攻擊攻擊的防護行為。
跨站請求偽造(Cross Site Request Forgery),簡寫成「CSRF」或者「XSRF」,是一種挾持用戶所用瀏覽器,執(zhí)行非法操作的攻擊方法,也就是說,攻擊者利用「CSRF」漏洞偽造用戶操作,可實現(xiàn)例如購物、注銷等效果,還可以利用該漏洞配合產(chǎn)生其他多種攻擊方式。
針對「CSRF」攻擊最經(jīng)濟的解決方式是增加「Referer」頭或者增加校驗「Token」。
Spring Security 提供了針對「CSRF」的規(guī)范化防御手段,本節(jié)我們主要討論如何使用 Spring Security 的內(nèi)置功能防御「CSRF」攻擊。
2. CSRF 攻擊原理
我們用一個實例演示「CSRF」攻擊的過程。
假設(shè)我們登陸了一個銀行網(wǎng)站(bank.example.com),這個網(wǎng)站的作用是實現(xiàn)跨行轉(zhuǎn)賬的表單提交,通常情況下,我們會生成如下一個 Form 表單。
<form method="post" action="/transfer">
<!-- 匯款金額 -->
<input type="text" name="amount"/>
<!-- 匯款路由號 -->
<input type="text" name="routingNumber"/>
<!-- 匯款賬戶 -->
<input type="text" name="account"/>
<input type="submit" value="提交"/>
</form>
那我們發(fā)出的「post」請求格式可能如下:
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
此時,如果我們未登出,并且訪問了其他惡意網(wǎng)站,并且其他惡意網(wǎng)站同樣包含了可提交的表單,表單形式如下:
<form method="post" action="https://bank.example.com/transfer">
<!-- 隱藏項不可見,轉(zhuǎn)賬金額,固定 100 元 -->
<input type="hidden" name="amount" value="100.00"/>
<!-- 隱藏項不可見,轉(zhuǎn)賬路由碼 -->
<input type="hidden" name="routingNumber" value="evilsRoutingNumber"/>
<!-- 隱藏項不可見,轉(zhuǎn)賬賬戶 -->
<input type="hidden" name="account" value="evilsAccountNumber"/>
<!-- 可見 -->
<input type="submit" value="快來點我!"/>
</form>
當我們很好奇,點擊了「快來點我」按鈕時,我們會觸發(fā)轉(zhuǎn)賬請求,并將錢匯款到一個未知賬戶里。在這個過程中,雖然惡意網(wǎng)站并不知道我們的「Cookies」值,但是由于未登出,我們和銀行網(wǎng)站之間的 Cookies 還在,所以當我們再次發(fā)起請求時,該 Cookies 依然有效,這使得不知不覺被觸發(fā)的轉(zhuǎn)賬請求同樣有效。
除此之外,如果惡意網(wǎng)站使用 JS 腳本自動提交表單的話,用戶可能沒有任何被攻擊的感覺。
3. CSRF 攻擊的防御
我們雖然無法阻止惡意網(wǎng)站向目標網(wǎng)站發(fā)送 HTTP 請求,但是我們可以確保惡意網(wǎng)站無法生成目標網(wǎng)站所需的參數(shù),所以就出現(xiàn)了如下兩種常見的解決方案:
- 使用同步「Token」模式
- 在 Cookies 中指定網(wǎng)站同源的參數(shù)
這兩種方式在 Spring Security 中都已支持。
3.1 CSRF 保護的前提
要實現(xiàn) CSRF 保護,首先我們要確保安全方法是冪等的。安全方法包括「GET」,「HEAD」,「OPTIONS」,「TRACE」,冪等是指這些方法在反復(fù)發(fā)送后服務(wù)器狀態(tài)不會改變。
3.2 同步「Token」模式
這種方法時防御「CSRF」攻擊的最常用手段,它的原理是確保項目表網(wǎng)站發(fā)送的請求中,每次都包含一個被稱為「CSRF Token」的隨機參數(shù)。
每當請求提交到服務(wù)端后,服務(wù)端會對比請求中包含的「CSRF Token」和期望的是否匹配,如果不匹配,此請求作廢。這里的關(guān)鍵是,「CSRF Token」不能由瀏覽器生成。
來看一下代碼:
<form method="post" action="/transfer">
<!-- csrf 頭 -->
<input type="hidden" name="_csrf" value="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/>
<!-- 匯款金額 -->
<input type="text" name="amount"/>
<!-- 匯款路由號 -->
<input type="text" name="routingNumber"/>
<!-- 匯款賬戶 -->
<input type="text" name="account"/>
<input type="submit" value="提交"/>
</form>
上述代碼中增加了 _csrf
參數(shù),且該值由服務(wù)器生成并埋在頁面表單中,此時 HTTP 請求的內(nèi)容如下:
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
由于瀏覽器同源策略,我們不用擔心惡意網(wǎng)站可以得到 _csrf
的值,而且惡意網(wǎng)站無法偽造出于服務(wù)器想匹配的「CSRF Token」,這樣就保護了用戶信息的安全。
3.3 SameSite 參數(shù)
另一種避免「CSRF」攻擊的方法是使用 SameSite
參數(shù)。這是一種新的方式,有賴于瀏覽器的支持。這種情況下服務(wù)端會指定一個 SameSite
參數(shù)來保障請求不會來自于非可信網(wǎng)站。
例如,包含 SameSite
參數(shù)的響應(yīng)消息如下:
Set-Cookie: JSESSIONID=randomid; Domain=bank.example.com; Secure; HttpOnly; SameSite=Lax
其中 SameSite
屬性支持的值包括:
- 嚴格模式(Strict)。僅 URL 相匹配的網(wǎng)站才支持發(fā)送 Cookie,任何其他網(wǎng)站都不會發(fā)生 Cookie。
- 寬松模式(Lax)。相比嚴格模式,寬松模式下支持部分非相同網(wǎng)站的請求中攜帶 Cookie,比如超鏈接方式跳轉(zhuǎn)、預(yù)加載、GET請求。
采用這種方式后,來自惡意網(wǎng)站的請求無法加上 JSESSIONID
參數(shù),由此避免了「CSRF」攻擊。
4. Spring Security 配置方法
默認情況下,Spring Security 已開啟「CSRF」保護,這里我們羅列一下其它常用配置。
4.1 自定義 Token 倉庫
默認情況下,CSRF Token 存儲在 HttpSession
中,使用 HttpSessionCsrfTokenRepository
對象維護。如果需要進行擴展,比如不僅要在 HTTP 請求中攜帶 Token,也需要在 JS 應(yīng)用中應(yīng)用 Token,那需要通過如下方式:
在配置類中構(gòu)造并注入 CookieCsrfTokenRepository 對象。
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) {
http.csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()));
}
}
4.2 禁用 CSRF 保護
默認情況下,CSRF 保護功能已被開啟,如果需要關(guān)閉,可通過如下方式配置:
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) {
http.csrf(csrf -> csrf.disable());
}
}
4.3 攜帶 CSRF Token
為了在 HTTP 請求中攜帶 CSRF Token,我們必須要對 HTTP Request 做一些配置,因為它默認是不會攜帶 CSRF 相關(guān)參數(shù)的。默認情況下,Spring Security 中有 CsrfFilter
判斷請求中是否有 _csrf
參數(shù),通常請求來自于兩種情況,F(xiàn)orm 表單提交或者 Ajax。
4.3.1 Form 表單提交
使用 Form 表單提交代碼時,我們需要在 Form 參數(shù)中增加一個隱藏項:_csrf
,例如:
<input type="hidden"
name="_csrf"
value="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/>
這里的 _csrf
有幾種配置方式:
-
自動注入
Spring Security 通過擴展 Spring 的
RequestDataValueProcessor
類,實現(xiàn)了RequestDataValueProcessor
類,這意味著如果我們使用 Spring 標簽庫、Thymeleaf 模板插件、或者其它集成了RequestDataValueProcessor
對象的視圖組件是,表單的非冪等請求(例如:POST
)都會自動攜帶 CSRF Token。 -
JSP 標簽
針對 JSP 作為頁面開發(fā)基礎(chǔ),我們可以直接使用 Spring 的表單標簽庫或者
CsrfInput
標簽。也可以通過更加直接的方式,在使用HttpServletRequest
屬性_csrf
,代碼如下:<c:url var="logoutUrl" value="/logout"/> <form action="${logoutUrl}" method="post"> <input type="submit" value="登出" /> <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/> </form>
4.3.2 Ajax 和 JSON 請求
如果使用 Javascript 做為請求提交方式,我們沒法直接使用 Http CSRF 參數(shù),取而代之的是使用 Http 頭的方式。這同樣也有幾種方法:
-
自動注入
Spring Security 可以自動將 CSRF Token 保存到 Cookie 中,一些客戶端框架如 AngularJS 會自動從中得到 CSRF Token 并放置到請求頭中。
-
Meta 標簽
另一種方式是從 Cookie 中解壓 Token 并使用 Meta 標簽,如下:
<html> <head> <meta name="_csrf" content="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/> <meta name="_csrf_header" content="X-CSRF-TOKEN"/> <!-- ... --> </head>
當 Meta 標簽中有 Token 信息時,我們就可以將 Meta 中的 CSRF Token 值用作請求參數(shù)了。以 JQuery 為例:
$(function () { var token = $("meta[name='_csrf']").attr("content"); var header = $("meta[name='_csrf_header']").attr("content"); $(document).ajaxSend(function(e, xhr, options) { xhr.setRequestHeader(header, token); }); });
5. 小結(jié)
本節(jié)我們討論了 CSRF 的含義及在 Spring Security 中配置方法:
- CSRF 是一種常見的 B/S 攻擊形式;
- CSRF 可以在瀏覽器上偽造用戶的請求;
- CSRF 的防御思路是確保發(fā)送請求的來源是可信的;
- CSRF 的防御方法包含「同步 Token」和「設(shè)置 SameSite 參數(shù)」兩種,其中「SameSite」參數(shù)方式需要瀏覽器的支持;
- Spring Security 默認已開啟 CSRF 保護。
下節(jié)我們討論 Spring Security 對 HTTP 請求常用的和安全相關(guān)的頭部信息。