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 Architecture


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM