csrf(cross site request forgery)跨站請(qǐng)求偽造與springSecurity解決方案
是什么
CSRF 利用了系統(tǒng)對(duì)登錄期用戶的信任烫饼,使得用戶執(zhí)行了某些并非意愿的操作從而造成損失
案例:
1 你在某個(gè)商城網(wǎng)站登錄了媒峡,服務(wù)端返回一個(gè)記錄你登錄狀態(tài)的cookie
2 這個(gè)時(shí)候你桌面突然出來一個(gè)廣告頁(yè)面(點(diǎn)擊進(jìn)入xx網(wǎng)頁(yè)游戲)敢艰,你點(diǎn)擊了以后其骄,跳入B網(wǎng)站颓芭,同時(shí)B網(wǎng)站利用你網(wǎng)站記錄的session信息膊毁,發(fā)送購(gòu)買POST請(qǐng)求給商城網(wǎng)站丰歌,下單充值游戲
3 商城網(wǎng)站看到這是你登錄的session惰帽,是有效的橡淆,執(zhí)行這次請(qǐng)求召噩,為騙子充值
4 你莫名其妙發(fā)現(xiàn)自己明明什么沒有做,卻有充值訂單和錢被扣除
如何防御
使用POST請(qǐng)求時(shí)逸爵,確實(shí)避免了如img具滴、script、iframe等標(biāo)簽自動(dòng)發(fā)起GET請(qǐng)求的問題师倔,但這并不能杜絕CSRF攻擊的發(fā)生构韵。一些惡意網(wǎng)站會(huì)通過表單的形式構(gòu)造攻擊請(qǐng)求
HTTP Refer
HTTP Referer是header的一部分,當(dāng)瀏覽器向web服務(wù)器發(fā)送請(qǐng)求的時(shí)候趋艘,會(huì)帶上Referer疲恢,通過驗(yàn)證Referer,可以判斷請(qǐng)求的合法性瓷胧,如果Referer是其他網(wǎng)站的話显拳,就有可能是CSRF攻擊,則拒絕該請(qǐng)求
//獲取header中的referrer
String refer = request.getHeader("referer");
//獲取當(dāng)前網(wǎng)站地址
String sourceAddress = request.getScheme())+"://"+request.getServerName();
if(Stringutils.isBlank(refer)||refer.lastIndexOf(String.valueOf(sourceAddress))==0){
return false;
}
缺點(diǎn)
這種方式簡(jiǎn)單便捷搓萧,但并非完全可靠杂数。除前面提到的部分瀏覽器可以篡改 HTTP Referer外宛畦,如果用戶在瀏覽器中設(shè)置了不被跟蹤,那么HTTP Referer字段就不會(huì)自動(dòng)添加揍移,當(dāng)合法用戶訪問時(shí)次和,系統(tǒng)會(huì)認(rèn)為是CSRF攻擊,從而拒絕訪問
我們知道正常的頁(yè)面跳轉(zhuǎn)羊精,瀏覽器都會(huì)自動(dòng)帶上Referer斯够,那么現(xiàn)在的問題就變成了什么情況下瀏覽器會(huì)不帶Referer?可以大致總結(jié)為3種情況:
- Refer的作用是指一個(gè)請(qǐng)求是從哪里鏈接過來喧锦,那么當(dāng)一個(gè)請(qǐng)求并不是由鏈接觸發(fā)產(chǎn)生的读规,那么自然也就不需要指定這個(gè)請(qǐng)求的鏈接來源,比如直接在瀏覽器的地址欄中輸入一個(gè)資源的URL地址(GET),那么這種請(qǐng)求是不會(huì)包含Referer字段的燃少,因?yàn)檫@是一個(gè)“憑空產(chǎn)生”的HTTP請(qǐng)求束亏,并不是從一個(gè)地方鏈接過去的
- 跨協(xié)議間提交請(qǐng)求。常見的協(xié)議:ftp://,http://,https://,file://,javascript:,data:.最簡(jiǎn)單的情況就是我們?cè)诒镜卮蜷_一個(gè)HTML頁(yè)面阵具,這個(gè)時(shí)候?yàn)g覽器地址欄是file://開頭的碍遍,如果這個(gè)HTML頁(yè)面向任何http站點(diǎn)提交請(qǐng)求的話,這些請(qǐng)求的Referer都是空的阳液。那么我們接下來可以利用data:協(xié)議來構(gòu)造一個(gè)自動(dòng)提交的CSRF攻擊怕敬。當(dāng)然這個(gè)協(xié)議是IE不支持的,我們可以換用javascript:
- 用戶在瀏覽器中設(shè)置了不被跟蹤帘皿,那么HTTP Referer字段就不會(huì)自動(dòng)添加
Csrf Token
用戶登錄時(shí)东跪,系統(tǒng)發(fā)放一個(gè)CsrfToken值,用戶攜帶該CsrfToken值與用戶名鹰溜、密碼等參數(shù)完成登錄虽填。系統(tǒng)記錄該會(huì)話的 CsrfToken 值,之后在用戶的任何請(qǐng)求中曹动,都必須帶上該CsrfToken值斋日,并由系統(tǒng)進(jìn)行校驗(yàn)。
這種方法需要與前端配合墓陈,包括存儲(chǔ)CsrfToken值恶守,以及在任何請(qǐng)求中(包括表單和Ajax)攜帶CsrfToken值。安全性相較于HTTP Referer提高很多贡必,如果都是XMLHttpRequest熬的,則可以統(tǒng)一添加CsrfToken值;但如果存在大量的表單和a標(biāo)簽赊级,就會(huì)變得非常煩瑣。
SpringSecurity中使用Csrf Token
Spring Security通過注冊(cè)一個(gè)CsrfFilter來專門處理CSRF攻擊岔绸,在Spring Security中理逊,CsrfToken是一個(gè)用于描述Token值橡伞,以及驗(yàn)證時(shí)應(yīng)當(dāng)獲取哪個(gè)請(qǐng)求參數(shù)或請(qǐng)求頭字段的接口
public interface CsrfToken extends Serializable {
String getHeaderName();
String getParameterName();
String getToken();
}
//CsrfTokenRepository則定義了如何生成、保存以及加載CsrfToken晋被。
public interface CsrfTokenRepository {
CsrfToken generateToken(HttpServletRequest request);
void saveToken(CsrfToken token, HttpServletRequest request,
HttpServletResponse response);
CsrfToken loadToken(HttpServletRequest request);
}
在默認(rèn)情況下兑徘,Spring Security加載的是一個(gè)HttpSessionCsrfTokenRepository
HttpSessionCsrfTokenRepository 將 CsrfToken 值存儲(chǔ)在 HttpSession 中,并指定前端把CsrfToken 值放在名為“_csrf”的請(qǐng)求參數(shù)或名為“X-CSRF-TOKEN”的請(qǐng)求頭字段里(可以調(diào)用相應(yīng)的設(shè)置方法來重新設(shè)定)羡洛。校驗(yàn)時(shí)挂脑,通過對(duì)比HttpSession內(nèi)存儲(chǔ)的CsrfToken值與前端攜帶的CsrfToken值是否一致,便能斷定本次請(qǐng)求是否為CSRF攻擊欲侮。
<input type='hidden' name='${_csrf.parameterName}' value='${_csrf.token}'>
這種方式靈活性不足
Spring Security還提供了另一種方式崭闲,即CookieCsrfTokenRepository
CookieCsrfTokenRepository 是一種更加靈活可行的方案,它將 CsrfToken 值存儲(chǔ)在用戶的cookie內(nèi)威蕉。減少了服務(wù)器HttpSession存儲(chǔ)的內(nèi)存消耗刁俭,并且當(dāng)用cookie存儲(chǔ)CsrfToken值時(shí),前端可以用JavaScript讀热驼恰(需要設(shè)置該cookie的httpOnly屬性為false)牍戚,而不需要服務(wù)器注入?yún)?shù),在使用方式上更加靈活虑粥。
存儲(chǔ)在cookie中是不可以被CSRF利用的如孝,cookie 只有在同域的情況下才能被讀取,所以杜絕了第三方站點(diǎn)跨域獲取 CsrfToken 值的可能娩贷。CSRF攻擊本身是不知道cookie內(nèi)容的第晰,只是利用了當(dāng)請(qǐng)求自動(dòng)攜帶cookie時(shí)可以通過身份驗(yàn)證的漏洞。但服務(wù)器對(duì) CsrfToken 值的校驗(yàn)并非取自 cookie育勺,而是需要前端手動(dòng)將CsrfToken值作為參數(shù)攜帶在請(qǐng)求里
下面是csrfFilter的過濾過程
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
request.setAttribute(HttpServletResponse.class.getName(), response);
//獲取到cookie中的csrf Token(CookieTokenRepository)或者從session中獲鹊纭(HttpSessionCsrfTokenRepository)
CsrfToken csrfToken = this.tokenRepository.loadToken(request);
final boolean missingToken = csrfToken == null;
//加載不到,則證明請(qǐng)求是首次發(fā)起的涧至,應(yīng)該生成并保存一個(gè)新的 CsrfToken 值
if (missingToken) {
csrfToken = this.tokenRepository.generateToken(request);
this.tokenRepository.saveToken(csrfToken, request, response);
}
request.setAttribute(CsrfToken.class.getName(), csrfToken);
request.setAttribute(csrfToken.getParameterName(), csrfToken);
//排除部分不需要驗(yàn)證CSRF攻擊的請(qǐng)求方法(默認(rèn)忽略了GET腹躁、HEAD、TRACE和OPTIONS)
if (!this.requireCsrfProtectionMatcher.matches(request)) {
filterChain.doFilter(request, response);
return;
}
//實(shí)際的token從header或者parameter中獲取
String actualToken = request.getHeader(csrfToken.getHeaderName());
if (actualToken == null) {
actualToken = request.getParameter(csrfToken.getParameterName());
}
if (!csrfToken.getToken().equals(actualToken)) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Invalid CSRF token found for "
+ UrlUtils.buildFullRequestUrl(request));
}
if (missingToken) {
this.accessDeniedHandler.handle(request, response,
new MissingCsrfTokenException(actualToken));
}
else {
this.accessDeniedHandler.handle(request, response,
new InvalidCsrfTokenException(csrfToken, actualToken));
}
return;
}
filterChain.doFilter(request, response);
}
我們?cè)谌粘J褂弥心吓睿梢圆捎胔eader或者param的方式添加csrf_token纺非,下面示范從cookie中獲取token
<form action="/executeLogin" method="post">
<p>Sign in to continue</p>
<div class="lowin-group">
<label>用戶名 <a href="#" class="login-back-link">Sign in?</a></label>
<input type="text" name="username" class="lowin-input">
</div>
<div class="lowin-group password-group">
<label>密碼 <a href="#" class="forgot-link">Forgot Password?</a></label>
<input type="password" name="password" class="lowin-input">
</div>
<div class="lowin-group">
<label>驗(yàn)證碼</label>
<input type="text" name="kaptcha" class="lowin-input">
<img src="/kaptcha.jpg" alt="kaptcha" height="50px" width="150px" style="margin-left: 20px">
</div>
<div class="lowin-group">
<label>記住我</label>
<input name="remember-me" type="checkbox" value="true" />
</div>
<input type="hidden" name="_csrf">
<input class="lowin-btn login-btn" type="submit">
</form>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script>
$(function () {
var aCookie = document.cookie.split("; ");
console.log(aCookie);
for (var i=0; i < aCookie.length; i++)
{
var aCrumb = aCookie[i].split("=");
if ("XSRF-TOKEN" == aCrumb[0])
$("input[name='_csrf']").val(aCrumb[1]);
}
});
</script>
注意事項(xiàng)
springSecurity配置了默認(rèn)放行, 不需要通過csrfFilter過濾器檢測(cè)的http訪問方式
private static final class DefaultRequiresCsrfMatcher implements RequestMatcher {
private final HashSet<String> allowedMethods = new HashSet<>(
Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS"));
@Override
public boolean matches(HttpServletRequest request) {
return !this.allowedMethods.contains(request.getMethod());
}
}
之所以會(huì)有上面默認(rèn)的GET,HEAD,TRACE,OPTIONS方式赘方,是因?yàn)?/p>
如果這個(gè)http請(qǐng)求是通過get方式發(fā)起的請(qǐng)求烧颖,意味著它只是訪問服務(wù)器 的資源,僅僅只是查詢窄陡,沒有更新服務(wù)器的資源炕淮,所以對(duì)于這類請(qǐng)求,spring security的防御策略是允許的跳夭;
如果這個(gè)http請(qǐng)求是通過post請(qǐng)求發(fā)起的涂圆, 那么spring security是默認(rèn)攔截這類請(qǐng)求的
因?yàn)檫@類請(qǐng)求是帶有更新服務(wù)器資源的危險(xiǎn)操作们镜,如果惡意第三方可以通過劫持session id來更新 服務(wù)器資源,那會(huì)造成服務(wù)器數(shù)據(jù)被非法的篡改润歉,所以這類請(qǐng)求是會(huì)被Spring security攔截的模狭,在默認(rèn)的情況下,spring security是啟用csrf 攔截功能的踩衩,這會(huì)造成嚼鹉,在跨域的情況下,post方式提交的請(qǐng)求都會(huì)被攔截?zé)o法被處理(包括合理的post請(qǐng)求)驱富,前端發(fā)起的post請(qǐng)求后端無法正常 處理锚赤,雖然保證了跨域的安全性,但影響了正常的使用萌朱,如果關(guān)閉csrf防護(hù)功能宴树,雖然可以正常處理post請(qǐng)求,但是無法防范通過劫持session id的非法的post請(qǐng)求晶疼,所以spring security為了正確的區(qū)別合法的post請(qǐng)求酒贬,采用了token的機(jī)制 。