SpringSecurity的防Csrf攻擊


CSRF(Cross-site request forgery)跨站請求偽造,也被稱為One Click Attack或者Session Riding,通常縮寫為CSRFXSRF,是一種對網站的惡意利用。盡管聽起來像跨站腳本(XSS),但它與XSS非常不同,XSS利用站點內的信任用戶,而CSRF則通過偽裝成受信任用戶的請求來利用受信任的網站。與XSS攻擊相比,CSRF攻擊往往不大流行(因此對其進行防范的資源也相當稀少)和難以防范,所以被認為比XSS更具危險性。 
CSRF是一種依賴web瀏覽器的、被混淆過的代理人攻擊(deputy attack)。

如何防御

使用POST請求時,確實避免了如img、script、iframe等標簽自動發起GET請求的問題,但這並不能杜絕CSRF攻擊的發生。一些惡意網站會通過表單的形式構造攻擊請求

public final class CsrfFilter extends OncePerRequestFilter {
    public static final RequestMatcher DEFAULT_CSRF_MATCHER = new
            CsrfFilter.DefaultRequiresCsrfMatcher();
    private final Log logger = LogFactory.getLog(this.getClass());
    private final CsrfTokenRepository tokenRepository;
    private RequestMatcher requireCsrfProtectionMatcher;
    private AccessDeniedHandler accessDeniedHandler;
    public CsrfFilter(CsrfTokenRepository csrfTokenRepository) {
        this.requireCsrfProtectionMatcher = DEFAULT_CSRF_MATCHER;
        this.accessDeniedHandler = new AccessDeniedHandlerImpl();
        Assert.notNull(csrfTokenRepository, "csrfTokenRepository cannot be null");
        this.tokenRepository = csrfTokenRepository;
    }
    //通過這里可以看出SpringSecurity的csrf機制把請求方式分成兩類來處理
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        request.setAttribute(HttpServletResponse.class.getName(), response);
        CsrfToken csrfToken = this.tokenRepository.loadToken(request);
        boolean missingToken = csrfToken == null;
        if (missingToken) {
            csrfToken = this.tokenRepository.generateToken(request);
            this.tokenRepository.saveToken(csrfToken, request, response);
        }
        request.setAttribute(CsrfToken.class.getName(), csrfToken);
        request.setAttribute(csrfToken.getParameterName(), csrfToken);
//第一類:"GET", "HEAD", "TRACE", "OPTIONS"四類請求可以直接通過
        if (!this.requireCsrfProtectionMatcher.matches(request)) {
            filterChain.doFilter(request, response);
        } else {
//第二類:除去上面四類,包括POST都要被驗證攜帶token才能通過
            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));
                }
            } else {
                filterChain.doFilter(request, response);
            }
        }
    }
    public void setRequireCsrfProtectionMatcher(RequestMatcher requireCsrfProtectionMatcher) {
        Assert.notNull(requireCsrfProtectionMatcher, "requireCsrfProtectionMatcher cannot be
        null");
        this.requireCsrfProtectionMatcher = requireCsrfProtectionMatcher;
    }
    public void setAccessDeniedHandler(AccessDeniedHandler accessDeniedHandler) {
        Assert.notNull(accessDeniedHandler, "accessDeniedHandler cannot be null");
        this.accessDeniedHandler = accessDeniedHandler;
    }
    private static final class DefaultRequiresCsrfMatcher implements RequestMatcher {
        private final HashSet<String> allowedMethods;
        private DefaultRequiresCsrfMatcher() {
            this.allowedMethods = new HashSet(Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS"));
    }
        public boolean matches(HttpServletRequest request) {
            return !this.allowedMethods.contains(request.getMethod());
        }
    }
}

禁用Csrf

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
//關閉打開的csrf保護
    .csrf().disable();
}
}

Csrf Token

用戶登錄時,系統發放一個CsrfToken值,用戶攜帶該CsrfToken值與用戶名、密碼等參數完成登錄。系統記錄該會話的 CsrfToken 值,之后在用戶的任何請求中,都必須帶上該CsrfToken值,並由系統進行校驗。
這種方法需要與前端配合,包括存儲CsrfToken值,以及在任何請求中(包括表單和Ajax)攜帶CsrfToken值。安全性相較於HTTP Referer提高很多,如果都是XMLHttpRequest,則可以統一添加CsrfToken值;但如果存在大量的表單和a標簽,就會變得非常煩瑣。

SpringSecurity中使用Csrf Token

Spring Security通過注冊一個CsrfFilter來專門處理CSRF攻擊,在Spring Security中,CsrfToken是一個用於描述Token值,以及驗證時應當獲取哪個請求參數或請求頭字段的接口

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);
}

 HttpSessionCsrfTokenRepository

在默認情況下,Spring Security加載的是一個HttpSessionCsrfTokenRepository
HttpSessionCsrfTokenRepository 將 CsrfToken 值存儲在 HttpSession 中,並指定前端把CsrfToken 值放在名為“_csrf”的請求參數或名為“X-CSRF-TOKEN”的請求頭字段里(可以調用相應的設置方法來重新設定)。校驗時,通過對比HttpSession內存儲的CsrfToken值與前端攜帶的CsrfToken值是否一致,便能斷定本次請求是否為CSRF攻擊。

<input type='hidden' name='${_csrf.parameterName}' value='${_csrf.token}'>

 

這種方式在某些單頁應用中局限性比較大,靈活性不足。

CookieCsrfTokenRepository

Spring Security還提供了另一種方式,即CookieCsrfTokenRepository
CookieCsrfTokenRepository 是一種更加靈活可行的方案,它將 CsrfToken 值存儲在用戶的cookie內。減少了服務器HttpSession存儲的內存消耗,並且當用cookie存儲CsrfToken值時,前端可以用JavaScript讀取(需要設置該cookie的httpOnly屬性為false),而不需要服務器注入參數,在使用方式上更加靈活。

存儲在cookie中是不可以被CSRF利用的,cookie 只有在同域的情況下才能被讀取,所以杜絕了第三方站點跨域獲取 CsrfToken 值的可能。CSRF攻擊本身是不知道cookie內容的,只是利用了當請求自動攜帶cookie時可以通過身份驗證的漏洞。但服務器對 CsrfToken 值的校驗並非取自 cookie,而是需要前端手動將CsrfToken值作為參數攜帶在請求里

下面是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;
                //加載不到,則證明請求是首次發起的,應該生成並保存一個新的 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);

                //排除部分不需要驗證CSRF攻擊的請求方法(默認忽略了GET、HEAD、TRACE和OPTIONS)
        if (!this.requireCsrfProtectionMatcher.matches(request)) {
            filterChain.doFilter(request, response);
            return;
        }

                //實際的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);
    }

用戶想要堅持CSRF Token在cookie中。 默認情況下CookieCsrfTokenRepository將編寫一個名為 XSRF-TOKEN的cookie和從頭部命名 X-XSRF-TOKEN中讀取或HTTP參數 _csrf。

 
//代碼如下:
 
.and().csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())

 

我們在日常使用中,可以采用header或者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>驗證碼</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>

注意事項

springSecurity配置了默認放行, 不需要通過csrfFilter過濾器檢測的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());
        }
    }

之所以會有上面默認的GET,HEAD,TRACE,OPTIONS方式,是因為

  1. 如果這個http請求是通過get方式發起的請求,意味着它只是訪問服務器 的資源,僅僅只是查詢,沒有更新服務器的資源,所以對於這類請求,spring security的防御策略是允許的;

  2. 如果這個http請求是通過post請求發起的, 那么spring security是默認攔截這類請求的
    因為這類請求是帶有更新服務器資源的危險操作,如果惡意第三方可以通過劫持session id來更新 服務器資源,那會造成服務器數據被非法的篡改,所以這類請求是會被Spring security攔截的,在默認的情況下,spring security是啟用csrf 攔截功能的,這會造成,在跨域的情況下,post方式提交的請求都會被攔截無法被處理(包括合理的post請求),前端發起的post請求后端無法正常 處理,雖然保證了跨域的安全性,但影響了正常的使用,如果關閉csrf防護功能,雖然可以正常處理post請求,但是無法防范通過劫持session id的非法的post請求,所以spring security為了正確的區別合法的post請求,采用了token的機制 。


免責聲明!

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



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