10.6.1?CSRF Attacks
在討論Spring Security?如何保護應(yīng)用程序免受CSRF攻擊之前,我們將解釋什么是CSRF攻擊。讓我們看一個具體的例子,以便更好地理解。
假設(shè)您的銀行網(wǎng)站提供了一個表單,允許將資金從當(dāng)前登錄的用戶轉(zhuǎn)移到另一個銀行帳戶。例如,HTTP請求可能如下所示:
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
現(xiàn)在假設(shè)你通過了銀行網(wǎng)站的身份驗證,然后,不注銷,訪問一個邪惡的網(wǎng)站。邪惡網(wǎng)站包含一個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="Win Money!"/>
</form>
你喜歡贏錢,所以你點擊提交按鈕。在此過程中,您無意中將100美元轉(zhuǎn)移給了惡意用戶。這是因為,雖然邪惡的網(wǎng)站看不到您的cookie,但與您的銀行相關(guān)聯(lián)的cookie仍然隨請求一起發(fā)送。
最糟糕的是,整個過程可以使用JavaScript實現(xiàn)自動化。這意味著你甚至不需要點擊按鈕。那么,我們?nèi)绾伪Wo自己免受這種攻擊呢?
10.6.2?Synchronizer Token Pattern
問題是,來自銀行網(wǎng)站的HTTP請求和來自邪惡網(wǎng)站的請求完全相同。這意味著無法拒絕來自邪惡網(wǎng)站的請求只允許來自銀行網(wǎng)站的請求。為了防止CSRF攻擊,我們需要確保邪惡站點無法提供請求中的某些內(nèi)容。
一種解決方案是使用 ?Synchronizer Token Pattern。這個解決方案是為了確保除了會話cookie之外,每個請求都需要一個隨機生成的令牌作為HTTP參數(shù)。提交請求時,服務(wù)器必須查找參數(shù)的預(yù)期值,并將其與請求中的實際值進行比較。如果值不匹配,則請求應(yīng)失敗。
我們可以放寬期望,只需要更新狀態(tài)的每個HTTP請求的令牌。這可以安全地完成,因為同源策略確保邪惡的站點無法讀取響應(yīng)。此外,我們不希望在HTTP GET中包含隨機令牌,因為這可能導(dǎo)致令牌泄漏。
讓我們看看我們的示例將如何改變。假設(shè)隨機生成的令牌存在于名為 _csrf 的HTTP參數(shù)中。例如,轉(zhuǎn)賬請求如下:
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參數(shù)?,F(xiàn)在邪惡網(wǎng)站無法猜測CSRF參數(shù)的正確值(必須在邪惡網(wǎng)站上明確提供),當(dāng)服務(wù)器將實際令牌與預(yù)期令牌進行比較時,傳輸將失敗。
10.6.3?When to use CSRF protection
什么時候應(yīng)該使用CSRF保護?我們的建議是對正常用戶可以通過瀏覽器處理的任何請求使用CSRF保護。如果您只創(chuàng)建一個非瀏覽器客戶端使用的服務(wù),那么您可能希望禁用CSRF保護。
CSRF protection and JSON
一個常見的問題是,“我需要保護JavaScript發(fā)出的JSON請求嗎?”簡而言之,這要看情況而定。但是,您必須非常小心,因為存在可能影響JSON請求的CSRF漏洞。例如,惡意用戶可以使用以下表單使用JSON創(chuàng)建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結(jié)構(gòu)
{?
"amount": 100,
"routingNumber": "evilsRoutingNumber",
"account": "evilsAccountNumber",
"ignore_me": "=test"
}
如果應(yīng)用程序沒有驗證 Content-Type,那么它將暴露于此漏洞中。根據(jù)設(shè)置的不同,驗證 Content-Type 的SpringMVC應(yīng)用程序仍然可以通過更新url后綴以“.json”結(jié)尾來利用,如下所示:
<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 and Stateless Browser Applications
如果我的應(yīng)用程序是無狀態(tài)的呢?這并不一定意味著你受到了保護。事實上,如果用戶對于給定的請求不需要在Web瀏覽器中執(zhí)行任何操作,那么他們可能仍然容易受到CSRF攻擊。????
例如,假設(shè)一個應(yīng)用程序使用一個自定義cookie,該cookie包含它內(nèi)部的所有狀態(tài)來進行身份驗證,而不是JSessionID。當(dāng)進行CSRF攻擊時,定制cookie將與請求一起發(fā)送,發(fā)送方式與上一個示例中發(fā)送JSessionID cookie的方式相同。
使用 basic 認證的用戶也容易受到CSRF攻擊,因為瀏覽器將自動在任何請求中包含用戶名密碼,其方式與我們上一個示例中發(fā)送JSessionID cookie的方式相同。
10.6.4?Using Spring Security CSRF Protection
那么,使用Spring Security保護我們的站點免受CSRF攻擊所必需的步驟是什么?使用Spring Security的CSRF保護的步驟概述如下:????
Use proper HTTP verbs?使用正確的HTTP動詞
防止CSRF攻擊的第一步是確保您的網(wǎng)站使用正確的HTTP動詞。具體來說,在Spring Security的CSRF支持可以使用之前,您需要確保您的應(yīng)用程序正在使用 ?PATCH、POST、PUT和/或 DELETE 來修改狀態(tài)。
這不是Spring Security支持的限制,而是正確預(yù)防CSRF的一般要求。原因是在HTTP GET中包含私有信息會導(dǎo)致信息泄漏。請參閱 ?RFC 2616 Section 15.1.3 Encoding Sensitive Information in URI’s?,以獲取有關(guān)使用POST而不是GET獲取敏感信息的一般指導(dǎo)。
Configure CSRF Protection?配置CSRF保護
下一步是在你的應(yīng)用程序中引入Spring Security 的CSRF保護。一些框架通過使用戶的會話無效來處理無效的CSRF令牌,但這會導(dǎo)致自身的問題。相反,默認情況下,Spring Security的CSRF保護將導(dǎo)致HTTP 403訪問被拒絕。這可以通過配置 AccessDeniedHandler 以不同方式處理 InvalidCsrfTokenException?來定制。
從Spring Security 4.0開始,CSRF保護默認通過XML配置啟用。如果要禁用CSRF保護,可以在下面看到相應(yīng)的XML配置。
<http>
????????<!-- ... -->
????????<csrfdisabled="true"/>
</http>
CSRF保護在使用Java配置時默認啟用。如果您想禁用CSRF,可以看下面相應(yīng)的Java配置。有關(guān)CSRF保護的其他自定義配置,請參閱csrf()的javadoc。
@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {
? ? @Override
? ? protected void configure(HttpSecurity http) throws Exception {
? ? ? ? http
? ? ? ? ? ? .csrf().disable();
? ? }
}
Include the CSRF Token
Form Submissions 表單提交
最后一步是確保在所有 PATCH, POST, PUT 和 DELETE? 方法中都包含CSRF令牌。一種方法是使用 _csrf 請求屬性獲取當(dāng)前 CsrfToken 。使用JSP執(zhí)行此操作的示例如下所示:
<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>
一種更簡單的方法是使用SpringSecurityJSP標(biāo)記庫中的?the csrfInput tag?標(biāo)記。
如果您使用的是spring mvc<form:form> tag或 Thymeleaf 2.1+,并且使用的是 @enableWebSecurity,則會自動為你包含 CsrfToken? (使用 CsrfRequestDataValueProcessor )。
Ajax and JSON Requests
如果你正在使用JSON,那么就不可能在HTTP參數(shù)中提交CSRF令牌。相反,您可以在HTTP頭中提交令牌。典型的模式是在 <meta> 中包含CSRF令牌。帶有JSP的示例如下所示:
<html>
<head>
? ? <meta name="_csrf" content="${_csrf.token}"/>
? ? <!-- default header name is X-CSRF-TOKEN -->
? ? <meta name="_csrf_header" content="${_csrf.headerName}"/>
? ? <!-- ... -->
</head>
<!-- ... -->
你可以使用Spring Security JSP標(biāo)記庫中更簡單的 ?csrfMetaTags tag,而不是手動創(chuàng)建meta標(biāo)記。
然后可以在所有Ajax請求中包含該令牌。如果您使用的是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);
????});
});
作為jquery的替代方案,我們建議使用cujojs的rest.js。js模塊為以RESTful方式處理HTTP請求和響應(yīng)提供了高級支持。核心功能是通過將攔截器鏈接到客戶機來根據(jù)需要添加行為,從而將HTTP客戶機上下文化的能力。
var client = rest.chain(csrf, {
token: $("meta[name='_csrf']").attr("content"),
name: $("meta[name='_csrf_header']").attr("content")
});
配置的客戶端可以與應(yīng)用程序的任何組件共享,這些組件需要向受CSRF保護的資源發(fā)出請求。rest.js和jquery之間的一個顯著區(qū)別是,只有使用配置的客戶端進行的請求才會包含CSRF令牌,而jquery中的所有請求都將包含該令牌。確定接收令牌的請求范圍的能力有助于防止CSRF令牌泄漏給第三方。有關(guān)rest.js的更多信息,請參閱rest.js參考文檔。
CookieCsrfTokenRepository
在某些情況下,用戶可能希望在cookie中保留 CsrfToken?。默認情況下,CookieCsrfTokenRepository?將在cookie中寫入名為 XSRF-TOKEN 的 token,并從名為 X-XSRF-TOKEN 的頭或http參數(shù) _csrf 讀取該 token。這些默認值來自AngularJS
可以使用以下方法在XML中配置 CookieCsrfTokenRepository?:
<http>
? ? <!-- ... -->
? ? <csrf token-repository-ref="tokenRepository"/>
</http>
<b:bean id="tokenRepository"
? ? class="org.springframework.security.web.csrf.CookieCsrfTokenRepository"
? ? p:cookieHttpOnly="false"/>
示例顯式設(shè)置 cookieHttpOnly=false。這是允許JavaScript(即AngularJS)讀取它所必需的。如果不需要直接使用javascript讀取cookie,建議省略 ?cookieHttpOnly=false?以提高安全性。
10.6.5?CSRF Caveats
在實施CSRF時有一些注意事項。
Timeouts
一個問題是預(yù)期的CSRF令牌存儲在 HttpSession 中,因此一旦 HttpSession 過期,配置的 AccessDeniedHandler?將收到一個無效的 InvalidCsrfTokenException。如果您使用的是默認的 AccessDeniedHandler,瀏覽器將得到一個HTTP403并顯示一條錯誤消息。
有人可能會問,為什么默認情況下預(yù)期的 CsrfToken? 沒有存儲在cookie中。這是因為存在已知的漏洞,其中頭(即指定cookie)可以由另一個域設(shè)置。這也是RubyonRails在存在頭X-requested-with時不再跳過CSRF檢查的原因。有關(guān)如何執(zhí)行該漏洞攻擊的詳細信息,請參閱此webappsec.org線程。另一個缺點是,通過刪除狀態(tài)(即超時),您將失去在令牌受到威脅時強制終止令牌的能力。
緩解活動用戶超時的一個簡單方法是使用一些JavaScript讓用戶知道他們的會話即將到期。用戶可以單擊按鈕繼續(xù)并刷新會話。
或者,指定一個自定義的accessdeniedHandler允許您以任何方式處理invalidCSRFtokenException。有關(guān)如何自定義AccessDeniedHandler?的示例,請參閱所提供的XML和Java配置的鏈接。
最后,可以將應(yīng)用程序配置為使用不會過期的 CookicsrftokenRepository。如前所述,這并不像使用會話那樣安全,但在許多情況下,這已經(jīng)足夠好了。
Logging In
為了防止偽造登錄請求,登錄表單也應(yīng)防止CSRF攻擊。由于 CsrfToken?存儲在 HttpSession 中,這意味著一旦訪問CsrfToken?屬性,就會創(chuàng)建 HttpSession?。雖然這在一個RESTful/無狀態(tài)的體系結(jié)構(gòu)中聽起來很糟糕,但事實是狀態(tài)對于實現(xiàn)真正的安全是必要的。沒有狀態(tài),一旦token被破壞我們就無能為力。實際上,CSRF令牌的大小非常小,對我們的體系結(jié)構(gòu)的影響應(yīng)該可以忽略不計。
保護登錄表單的常見技術(shù)是在表單提交之前使用javascript函數(shù)獲取有效的CSRF令牌。通過這樣做,無需考慮會話超時(在上一節(jié)中討論),因為會話是在表單提交之前創(chuàng)建的(假設(shè)沒有配置 CookieCsrfTokenRepository),因此用戶可以留在登錄頁面上,并在需要時提交用戶名/密碼。為了實現(xiàn)這一點,您可以利用Spring Security?提供的CsrfTokenArgumentResolver?,并像這里描述的那樣公開一個端點。
Logging Out
啟動了CSRF,將更新 LogoutFilter 只能使用 HTTP POST(不啟用CSRF時,GET請求也是可以正常退出的,因為其不需要CSRF token)。這可以確保注銷需要CSRF令牌,并且惡意用戶無法強制注銷您的用戶。
一種方法是使用表單注銷。如果你真的想要一個鏈接,你可以使用javascript讓鏈接執(zhí)行一個POST(也就是說,可能在一個隱藏的表單上)。對于禁用了javascript的瀏覽器,您可以讓鏈接把用戶帶到一個注銷的確認頁面(POST提交)。
如果您真的想在注銷時使用HTTP GET,可以這樣做,但請記住,通常不建議這樣做。例如,下面的Java配置將執(zhí)行注銷,URL ?/logout?是用任何HTTP方法請求的:
@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {
? ? @Override
? ? protected void configure(HttpSecurity http) throws Exception {
? ? ? ? http
? ? ? ? ? ? .logout()
? ? ? ? ? ? ? ? .logoutRequestMatcher(new AntPathRequestMatcher("/logout"));
? ? }
}
Multipart (file upload)
對 multipart/form-data 使用CSRF保護有兩個選項。每個選項都有其權(quán)衡。
?Placing MultipartFilter before Spring Security
在將Spring Security的CSRF保護與多部分文件上傳集成之前,請確保您可以先上傳而不需要CSRF保護。有關(guān)將多部分表單與Spring一起使用的更多信息,可以在?17.10 Spring’s multipart (file upload) support?章節(jié)和?MultipartFilter javadoc?中找到。
Placing MultipartFilter before Spring Security
第一個選項是確保在Spring Security 過濾器之前指定 MultipartFilter?。在Spring安全過濾器之前指定 MultipartFilter? 意味著沒有調(diào)用 MultipartFilter??的授權(quán),這意味著任何人都可以在服務(wù)器上放置臨時文件。但是,只有授權(quán)用戶才能提交由您的應(yīng)用程序處理的文件。一般來說,這是推薦的方法,因為臨時文件上載對大多數(shù)服務(wù)器都會產(chǎn)生不可忽略的影響。
為了確保在使用Java配置的Spring Security 過濾器之前指定?MultipartFilter?,用戶可以覆蓋beforeSpringSecurityFilterChain?,如下所示:
public class SecurityApplicationInitializer extends AbstractSecurityWebApplicationInitializer {
? ? @Override
? ? protected void beforeSpringSecurityFilterChain(ServletContext servletContext) {
? ? ? ? insertFilters(servletContext, new MultipartFilter());
? ? }
}
為了確保 MultipartFilter?在使用xml配置的spring?Security 過濾器之前被指定,用戶可以確保 ?MultipartFilter?的 <filter mapping>?元素放在web.xml中的 springSecurityFilterChain?之前,如下所示:
<filter>
? ? <filter-name>MultipartFilter</filter-name>
? ? <filter-class>org.springframework.web.multipart.support.MultipartFilter</filter-class>
</filter>
<filter>
? ? <filter-name>springSecurityFilterChain</filter-name>
? ? <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
? ? <filter-name>MultipartFilter</filter-name>
? ? <url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
? ? <filter-name>springSecurityFilterChain</filter-name>
? ? <url-pattern>/*</url-pattern>
</filter-mapping>
Include CSRF token in action
如果不允許未經(jīng)授權(quán)的用戶上傳臨時文件,則可以將 MultipartFilter?放在spring Security 過濾器之后,并將 CSRF?作為查詢參數(shù)包含在表單的action屬性中。下面是一個帶有JSP的示例
<form action="./upload?${_csrf.parameterName}=${_csrf.token}" method="post" enctype="multipart/form-data">
這種方法的缺點是可以泄漏查詢參數(shù)。一般來說,最好的做法是將敏感數(shù)據(jù)放在主體或頭中,以確保不會泄漏。其他信息可在?RFC 2616 Section 15.1.3 Encoding Sensitive Information in URI’s.中查找。
HiddenHttpMethodFilter
HiddenHttpMethodFilter?應(yīng)該放在Spring Security過濾器之前。一般來說,這是正確的,但在防止CSRF攻擊時,它可能會有額外的影響。
請注意,HiddenHttpMethodFilter?只重寫HTTP?的POST方法,因此這實際上不太可能導(dǎo)致任何實際問題。但是,最好還是將其放在Spring Security過濾器之前。
10.6.6?Overriding Defaults
SpringSecurity的目標(biāo)是提供保護您的用戶免受攻擊的默認值。這并不意味著您必須接受它的所有默認值。
例如,您可以提供一個自定義的 CsrfTokenRepository?來覆蓋 CSRFtoken 的存儲方式。為了保證在分布式系統(tǒng)中可以使用,用Redis來存儲是比較常見的實現(xiàn)方式。
您還可以指定一個自定義的 RequestMatcher?來確定哪些請求受CSRF保護(也就是說,您可能不關(guān)心logout是否安全)。簡而言之,如果Spring Security的CSRF保護行為不完全符合您的要求,那么您可以自定義該行為。有關(guān)如何使用XML和java配置來進行這些自定義的詳細信息,詳見the section called “<csrf>”章節(jié)。