目錄
1.什麼是CSRF攻擊
下面我們以一個具體的例子來說明這種常見的攻擊模式
1.1 假定某個銀行的網站提供讓當前登錄用戶給其他賬號轉賬的功能,轉賬請求的格式如下
POST /transfer HTTP/1.1
Host: bank.example.com
Cookie: JSESSIONID=randomid; Domain=bank.example.com; Secure; HttpOnly
Content-Type: application/x-www-form-urlencoded
amount=100.00&routingNumber=1234&account=9876
1.2 現在有一個用戶登錄到了這個銀行的網站並且進行了認證,在未logout的情況下訪問了一個惡意網站,惡意網站包含如下html代碼段
<form action="https://bank.example.com/transfer" method="post">
<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="領取獎品"/>
</form>
這時如果用戶點擊了【領取獎品】按鈕,你將給那個惡意攻擊者的賬戶轉入100元,這是因爲雖然惡意網站不能獲取到用戶cookie信息,但是當點擊【領取獎品】按鈕訪問銀行網站時,cookie信息還是會一起發送。更糟糕的是,藉助於javascript,上面的過程可以自動執行,不需要等待用戶點擊【領取獎品】按鈕,只要你打開了這個頁面,你的錢就被偷走了,這也是我們常見的釣魚網站的做法。
像這樣雖然用戶訪問的是另一個網站,但是這個網站卻僞裝成當前認證用戶訪問用戶正在訪問的網站以實現攻擊的方式稱爲CSRF。
2. 同步器令牌模式
2.1 從上面的例子可以看出,無論轉賬請求是從銀行自己的網站發出,還是從惡意網站發出,對於銀行的服務端來說內容是一樣的,所以單純從服務端來講我們沒法過濾掉那些惡意的請求。如果我們能採取一種措施讓銀行的正常頁面發請求時給服務器提供一個憑證,並且這個憑證是惡意網站所不能提供的,這樣服務器端就可以很容器拒絕掉那些非法的請求了。
同步token就是這樣的一種方式,他要求在客戶端發起請求時,除了cookie信息外,還需要提供一個隨機的token值作爲參數。當服務器收到一個請求後,會先解析出這個token值,再和期望的值進行比較,如果不匹配則拒絕提供服務。
2.2 在實際項目中,我們可以放寬上面的規則,只要求那些會修改信息的請求才提供token值,因爲根據同源策略,那些惡意網站是不能獲取到我們正常請求的響應結果的。追加完token後,我們的請求示例如下:
POST /transfer HTTP/1.1
Host: bank.example.com
Cookie: JSESSIONID=randomid; Domain=bank.example.com; Secure; HttpOnly
Content-Type: application/x-www-form-urlencoded
amount=100.00&routingNumber=1234&account=9876&_csrf=<secure-random>
這樣,因爲惡意網站不能狗提供_csrf對應的隨機值,僞造的請求將不會被服務器接受。
3.如何利用spring security防止csrf攻擊
3.1 採用正確的http動詞
動詞 | 作用 | 類比數據庫操作 |
---|---|---|
GET | 從服務器獲取信息 | select |
POST | 在服務器上新創建一個資源 | insert |
PUT | 更新服務器上的一個資源,本次請求包含完整的信息 | update |
PATCH | 更新服務器上的一個資源,包含部分信息 | update |
DELETE | 刪除服務器上的一個資源 | delete |
HEAD | 向服務器索要與GET請求相一致的響應,只不過只有頭部信息,響應體將不會被返回。 | 無 |
TRACE | 回顯服務器收到的請求,主要用於測試或診斷 | 無 |
OPTIONS | 返回服務器針對特定資源所支持的HTTP請求方法 | 無 |
確保對信息進行修改的請求其動詞一定是post、put、patch、delete中的一種。
3.2 配置CSRF
默認情況下,Java Configuration會啓用CSRF保護。如果要禁用CSRF,可以在下面看到相應的Java配置。有關如何配置CSRF保護的其他自定義,請參閱csrf()的Javadoc。
@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
}
}
一些框架通過使用戶的會話無效來處理無效的CSRF令牌,但這會導致其自身的問題。相反,默認情況下,Spring Security的CSRF保護將產生HTTP 403訪問被拒絕。這可以通過配置AccessDeniedHandler以進行InvalidCsrfTokenException不同的處理來自定義。自定義AccessDeniedHandler
3.3 包括CSRF令牌
我們要確保在所有執行post、put、patch、delete的請求中包含CSRF token值,最直接的方式是使jstl表達式從request中獲取到_csrf對應的值,如下
<c:url var="logoutUrl" value="/logout"/>
<form action="${logoutUrl}" method="post">
<input type="submit" value="Log out" />
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
</form>
另外spring 也爲我們提供了兩個方便的jsptag。具體參考例子
3.4 CookieCsrfTokenRepository
在某些場合下,我們可能會需要將csrf token值存儲在cookie中,此時可以用CookieCsrfTokenRepository來實現這個功能,默認情況下,寫入到cookie中的key是XSRF-TOKEN,讀取時從request header的X-XSRF-TOKEN中或者parameter的_csrf中讀取。可以用如下代碼段配置
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.csrfTokenRepository(new CookieCsrfTokenRepository());
}
}
默認情況下設置到cookie中的信息不能夠通過js讀取,如果需要js訪問的話需要明確設定
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
}
4 注意事項
4.1 超時
默認情況下,csrf token存儲在httpsession中,當session過期時AccessDeniedHandler會收到一個InvalidCsrfTokenException,如果我們使用的是spring security默認的AccessDeniedHandler,客戶端將會收到一個簡單的403錯誤(因爲 CSRF_FILTER在認證filter的後面),對於這個問題,我們可以採用下面幾種方式處理
- 在頁面裏面追加js,當session快過期時通知用戶,可以通過用戶點擊按鈕的方式刷新session
- 自定義AccessDeniedHandle,自己處理InvalidCsrfTokenException
- 也可以將csrf token存入CsrfTokenRepository中(如CookieCsrfTokenRepository),這樣就不會有過期問題,雖然存入token中有安全隱患,不過大多數情況下都不會有問題
4.2 登錄問題 ?
如果在登錄頁面也啓用csrf token保護,就需要在登錄前生成CsrfToken時創建HttpSession,這時就需要考慮如果用戶在登錄頁面長時間停留,會引起session過期問題,當登錄時直接返回403(沒權限登錄–),現在通用的解決方法是採用JavaScript在點擊登錄時,先獲取token值,接着在提交登錄請求,這樣session在登錄時才創建,用戶就可以在登錄界面停留任意時間了,利用CsrfTokenArgumentResolver我們很容易實現這樣的功能
4.3 Logout ??
默認情況下,啓用csrf token後,LogoutFilter只接收Post請求,並且logout時還需要提供csrf token值
@SuppressWarnings("unchecked")
private RequestMatcher getLogoutRequestMatcher(H http) {
if (logoutRequestMatcher != null) {
return logoutRequestMatcher;
}
if (http.getConfigurer(CsrfConfigurer.class) != null) {
this.logoutRequestMatcher = new AntPathRequestMatcher(this.logoutUrl, "POST");
}
else {
this.logoutRequestMatcher = new OrRequestMatcher(
new AntPathRequestMatcher(this.logoutUrl, "GET"),
new AntPathRequestMatcher(this.logoutUrl, "POST"),
new AntPathRequestMatcher(this.logoutUrl, "PUT"),
new AntPathRequestMatcher(this.logoutUrl, "DELETE")
);
}
return this.logoutRequestMatcher;
}
如果Logout操作安全性沒有那麼高,實現時不想這麼複雜,可以通過下面代碼段配置
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.logout()
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"));
}
}
這時可以用任意的HTTP method執行logout操作
4.4 文件上傳
可通過以下兩種方式解決
- 將MultipartFilter放到spring security 相關filter前面
public class SecurityApplicationInitializer extends AbstractSecurityWebApplicationInitializer {
@Override
protected void beforeSpringSecurityFilterChain(ServletContext servletContext) {
insertFilters(servletContext, new MultipartFilter());
}
}
- 在上傳文件對應的action中追加token值
<form action="./upload?${_csrf.parameterName}=${_csrf.token}" method="post" enctype="multipart/form- data">