Java 安全之:csrf防禦實戰分析

  上文總結了csrf攻擊以及一些經常使用的防禦方式,csrf全稱Cross-site request forgery(跨站請求僞造),是一類利用信任用戶已經獲取的註冊憑證,繞事後臺用戶驗證,向被攻擊網站發送未被用戶受權的跨站請求以對被攻擊網站執行某項操做的一種惡意攻擊方式。html

  上面的定義比較抽象,咱們先來舉一個簡單的例子來詳細解釋一下csrf攻擊,幫助理解。前端

  假設你經過電腦登陸銀行網站進行轉帳,通常這類轉帳頁面實際上是一個form表單,點擊轉帳其實就是提交表單,向後臺發起http請求,請求的格式大概像下面這個樣子:web

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表單,以下所示:ajax

<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的攻擊方式,雖然其不知道你的登陸信息,可是其利用瀏覽器自身的機制來冒充用戶繞事後臺用戶驗證從而發起攻擊。spring

  csrf是一種常見的web攻擊方式,一些現有的安全框架中都對該攻擊的防禦提供了支持,好比spring security,從4.0開始,默認就會啓用CSRF保護,會針對PATCH,POST,PUT和DELETE方法進行防禦。本文會結合spring security提供的防禦方法,並結合其源碼來學習一下其內部防禦原理,本文涉及到的Spring Security源碼版本爲5.1.5。後端

  本文目錄以下:瀏覽器

  使用Spring Security防禦CSRF攻擊安全

  Spring Security的CSRF防禦原理cookie

  總結session

 

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

相關文章
相關標籤/搜索