Java 安全之:csrf防護實戰分析
上文總結了csrf攻擊以及一些常用的防護方式,csrf全稱Cross-site request forgery(跨站請求偽造),是一類利用信任使用者已經獲取的註冊憑證,繞過後臺使用者驗證,向被攻擊網站傳送未被使用者授權的跨站請求以對被攻擊網站執行某項操作的一種惡意攻擊方式。
上面的定義比較抽象,我們先來舉一個簡單的例子來詳細解釋一下csrf攻擊,幫助理解。
假設你通過電腦登入銀行網站進行轉賬,一般這類轉賬頁面其實是一個form表單,點選轉賬其實就是提交表單,向後臺發起http請求,請求的格式大概像下面這個樣子:
POST /transfer HTTP/1.1 Host: xxx.bank.com Cookie: JSESSIONID=randomid; Domain=xxx.bank.com; Secure; HttpOnly Content-Type: application/x-www-form-urlencoded amount=100.00&routingNumber=1234&account=8888
好了,現在給自己的賬戶轉完賬了,但是這時你一般不會立馬退出銀行網站的登入,你可能會緊接著去上網瀏覽別的網頁,碰巧你上網的時候看到一些很吸引人眼球的廣告(比如在家兼職輕鬆月入上萬。。。之類的),你點選了一下,但是發現什麼也沒有,也許你會關掉這個網頁,以為什麼都沒有發生。但是後臺可能已經發生了一系列的事情,如果這是個釣魚網站,並且剛才你點選的頁面恰好又包含一個form表單,如下所示:
<form action="https://xxx.bank.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元(這些都可通過js實現自動化),而且是未經過你的授權的情況下,這就是csrf的攻擊方式,雖然其不知道你的登入資訊,但是其利用瀏覽器自身的機制來冒充使用者繞過後臺使用者驗證從而發起攻擊。
csrf是一種常見的web攻擊方式,一些現有的安全框架中都對該攻擊的防護提供了支援,比如spring security,從4.0開始,預設就會啟用CSRF保護,會針對PATCH,POST,PUT和DELETE方法進行防護。本文會結合spring security提供的防護方法,並結合其原始碼來學習一下其內部防護原理,本文涉及到的Spring Security原始碼版本為5.1.5。
本文目錄如下:
使用Spring Security防護CSRF攻擊
Spring Security的CSRF防護原理
總結
1. 使用Spring Security防護CSRF攻擊
通過Spring Security來防護CSRF攻擊需要做哪些配置呢,總結如下:
- 使用合適的HTTP請求方式
- 配置CSRF保護
- 使用CSRF Token
1.1 使用合適的HTTP請求方式
第一步是確保要保護的網站暴露的介面使用合適的HTTP請求方式,就是在還未開啟Security的CSRF之前需要確保所有的介面都只支援使用PATCH、POST、PUT、DELETE這四種請求方式之一來修改後端資料。
這並不是Spring Security在防護CSRF攻擊方面的自身限制,而是合理防護CSRF攻擊所必須做的,原因是通過GET的方式傳遞私有資料容易導致其洩露,使用POST來傳遞敏感資料更合理。
1.2 配置CSRF保護
下一步就是將Spring Security引入你的後臺應用中。有些框架通過讓使用者session失效來處理無效的CSRF Token,但是這種方式是有問題的,取而代之,Spring Security預設返回一個403的HTTP狀態碼來拒絕無效訪問,可以通過配置AccessDeniedHandler來實現自己的拒絕邏輯。
如果專案中是採用的XML配置,則必須顯示的使用<csrf>標籤元素來開啟CSRF防護,詳見<csrf>。
通過Java配置的方式則會預設開啟CSRF防護,如果希望禁用這一功能,則需要手動配置,見下面的示例,更詳細的配置可以參考csrf()方法的官方文件。
@EnableWebSecurity @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable(); } }
1.3 使用CSRF Token
接下來就是在每次請求的時候帶上一個CSRF Token,根據請求的方式不同會有不同的方式:
1.3.1 Form表單提交
通過表單提交會將CSRF Token附在Http請求的_csrf屬性中,後臺介面從請求中獲取token,如下是一個示例(JSP):
<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>
其實就是後臺在渲染頁面時先生成一個CSRF Token,放到表單中;然後在使用者提交表單時就會附帶上這個CSRF Token,後臺將其取出來並進行校驗,不一致則拒絕這次請求。這裡因為這Token是後臺生成的,這對於第三方網站是獲取不到的,通過這種方式實現防護。
1.3.2 Ajax和JSON請求
如果是使用的JSON,則不需要將CSRF Token以HTTP引數的形式提交,而是放在HTTP請求頭中。典型的做法是將CSRF Token包含在在頁面的元標籤中。如下是一個JSP的例子:
<html> <head> <meta name="_csrf" content="${_csrf.token}"/> <!-- default header name is X-CSRF-TOKEN --> <meta name="_csrf_header" content="${_csrf.headerName}"/> <!-- ... --> </head> <!-- ... -->
然後在所有的Ajax請求中需要帶上CSRF Token,如下是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); }); });
到這裡所有的配置都已經好了,包括介面呼叫方式的設計、框架的配置、前端頁面的配置,前文中講了一系列的防護方式,Spring Security又是採用的什麼方式呢,最直接的方式就是看原始碼了。
2. Spring Security的CSRF防護原理
Spring Security是基於Filter(過濾器)來實現其安全功能的,關於CSRF防護的主要邏輯是在CsrfFilter這個過濾器中的,繼承自OncePerRequestFilter,並且重寫了doFilterInternal方法:
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { request.setAttribute(HttpServletResponse.class.getName(), response); // 通過tokenRepository從request中獲取csrf token CsrfToken csrfToken = this.tokenRepository.loadToken(request); final boolean missingToken = csrfToken == null; // 如果未獲取到token則新生成token並儲存 if (missingToken) { csrfToken = this.tokenRepository.generateToken(request); this.tokenRepository.saveToken(csrfToken, request, response); } request.setAttribute(CsrfToken.class.getName(), csrfToken); request.setAttribute(csrfToken.getParameterName(), csrfToken); // 判斷是否需要進行csrf token校驗 if (!this.requireCsrfProtectionMatcher.matches(request)) { filterChain.doFilter(request, response); return; } // 獲取前端傳過來的實際token String actualToken = request.getHeader(csrfToken.getHeaderName()); if (actualToken == null) { actualToken = request.getParameter(csrfToken.getParameterName()); } // 校驗兩個token是否相等 if (!csrfToken.getToken().equals(actualToken)) { if (this.logger.isDebugEnabled()) { this.logger.debug("Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request)); } // 如果是token缺失導致,則丟擲MissingCsrfTokenException異常 if (missingToken) { this.accessDeniedHandler.handle(request, response, new MissingCsrfTokenException(actualToken)); } // 如果不是同一個token則丟擲InvalidCsrfTokenException異常 else { this.accessDeniedHandler.handle(request, response, new InvalidCsrfTokenException(csrfToken, actualToken)); } return; } // 執行下一個過濾器 filterChain.doFilter(request, response); }
整個流程還是很清晰的,我們總結一下:
- 先通過tokenRepository從request中獲取csrf token;
- 如果未獲取到token則新生成token並儲存;
- 判斷是否需要進行csrf token校驗,不需要則直接執行下一個過濾器;
- 呼叫request的getHeader()方法或者getParameter()方法獲取前端傳過來的實際token;
- 校驗兩個token是否相等,不相等則丟擲異常,相等則校驗通過,執行下一個過濾器;
可以知道,Spring Security是藉助CSRF Token來實現防護的,上文我們講到,通過token的方式可以選擇cookie來儲存也可以選擇session的方式,那Spring Security提供了什麼方式呢,答案就在獲取token的tokenRepository中,我們看一下,這個tokenRepository型別是CsrfTokenRepository(這是一個介面),Spring Security提供了三種實現,分別是HttpSessionCsrfTokenRepository、CookieCsrfTokenRepository、LazyCsrfTokenRepository,我們著重看一下前兩者,顧名思義,一個是通過session,而另一個則是通過cookie,我們再分別看一下其各自實現的loadToken()方法,驗證一下。
// CookieCsrfTokenRepository中的實現 public CsrfToken loadToken(HttpServletRequest request) { Cookie cookie = WebUtils.getCookie(request, this.cookieName); if (cookie == null) { return null; } String token = cookie.getValue(); if (!StringUtils.hasLength(token)) { return null; } return new DefaultCsrfToken(this.headerName, this.parameterName, token); } // HttpSessionCsrfTokenRepository中的實現 public CsrfToken loadToken(HttpServletRequest request) { HttpSession session = request.getSession(false); if (session == null) { return null; } return (CsrfToken) session.getAttribute(this.sessionAttributeName); }
到這裡我們已經很清楚了,Spring Security提供多種儲存token的策略,既可以儲存在cookie中,也可以儲存在session中,這個可以手動指定。所以前文說到的兩個關於token的防護方式,Spring Security都支援。既然到這裡了,我們就再看一下Spring Security是如何生成和儲存token的,這裡僅以CookieCsrfTokenRepository的實現為例:
// 生成token public CsrfToken generateToken(HttpServletRequest request) { return new DefaultCsrfToken(this.headerName, this.parameterName, createNewToken()); } private String createNewToken() { return UUID.randomUUID().toString(); } // 儲存token public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) { String tokenValue = token == null ? "" : token.getToken(); Cookie cookie = new Cookie(this.cookieName, tokenValue); cookie.setSecure(request.isSecure()); if (this.cookiePath != null && !this.cookiePath.isEmpty()) { cookie.setPath(this.cookiePath); } else { cookie.setPath(this.getRequestContext(request)); } if (token == null) { cookie.setMaxAge(0); } else { cookie.setMaxAge(-1); } if (cookieHttpOnly && setHttpOnlyMethod != null) { ReflectionUtils.invokeMethod(setHttpOnlyMethod, cookie, Boolean.TRUE); } response.addCookie(cookie); }
可以看到,生成的token其實本質就是一個uuid,而儲存則是儲存在cookie中,涉及到cookie操作,其中有很多細節,本文就不詳述了。
3. 總結
本文先解釋了一個csrf攻擊的基本例子,然後介紹了使用Spring Security來防護csrf攻擊所需要的配置,最後再從Spring Security原始碼的角度學習了一下其是如何實現csrf防護的,基本原理還是通過token來實現,具體可以藉助於cookie和session的方式來實現。
注:本文涉及到的原始碼均來自Spring Security 5.1.5。
參考文獻:
Cross Site Request Forgery (CSRF)
Spring Security Architec