How does Spring Security prevent application from CSRF?

How does Spring Security prevent application from CSRF?

Question #

When I wrote a Spring Security & Oauth2 web application according to this official guide with Spring Boot 3.0.6 and Spring Security 6.0.3, I encountered a problem when I configured Spring Security CSRF:

 1@Override
 2protected void configure(HttpSecurity http) throws Exception {
 3	// @formatter:off
 4    http
 5        // ... existing code here
 6        .csrf(c -> c
 7            .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
 8        )
 9        // ... existing code here
10    // @formatter:on
11}

Logs below shows the error info:

1o.s.security.web.csrf.CsrfFilter: Invalid CSRF token found for http://localhost:8080/logout
2o.s.s.w.access.AccessDeniedHandlerImpl: Responding with 403 status code

CookieCsrfRepository is a implementation of CsrfTokenRepository, Which persists the CSRF token in a cookie named "XSRF-TOKEN" and reads from the header "X-XSRF-TOKEN" following the conventions of AngularJS. When using with AngularJS be sure to use withHttpOnlyFalse().

So what does attribute HttpOnly mean? This is an attribute of Cookie, once HttpOnly is set to true(default value), the cookie will be hidden from scripts on the client side.

So, we need to set HttpOnly to false on behalf of JS's Cookie visibility.

Debug #

I debugged and found that Spring Security 6 uses XorCsrfTokenRequestAttributeHandler to provides BREACH protection of the CsrfToken by default.

 1// XorCsrfTokenRequestAttributeHandler.getTokenValue
 2private static String getTokenValue(String actualToken, String token) {
 3    byte[] actualBytes;
 4    try {
 5        actualBytes = Base64.getUrlDecoder().decode(actualToken);
 6    }
 7    catch (Exception ex) {
 8        return null;
 9    }
10
11    byte[] tokenBytes = Utf8.encode(token);
12    int tokenSize = tokenBytes.length;
13    if (actualBytes.length < tokenSize) {
14        // return null here
15        return null;
16    }
17    // .... existing code here
18}

And then the CsrfFilter throws a error:

 1// CsrfFilter.doFilterInternal
 2protected void doFilterInternal
 3    (HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
 4			throws ServletException, IOException {
 5            
 6    // ...existing code here
 7    CsrfToken csrfToken = deferredCsrfToken.get();
 8    String actualToken = this.requestHandler.resolveCsrfTokenValue(request, csrfToken);
 9    if (!equalsConstantTime(csrfToken.getToken(), actualToken)) {
10        boolean missingToken = deferredCsrfToken.isGenerated();
11        this.logger.debug(
12                LogMessage.of(() -> "Invalid CSRF token found for " 
13                    + UrlUtils.buildFullRequestUrl(request)));
14                // ... existing code here
15
16    }
17    filterChain.doFilter(request, response);
18}

The XorCsrfTokenRequestAttributeHandler expects a XOR'ed CSRF token, but the CookieCsrfTokenRepository provided a normal one. That's the reason.

Solution #

After googled, I found this problem was fully discussed on this stack overflow question.

As described before, I realized that tutorial was based on Spring Boot 2. So this may be some 'new feature' problem.-:) Spring Security replaces XorCsrfTokenRequestAttributeHandler with CsrfTokenRequestAttributeHandler, which cause the problem, based on this issue.

Some one said that you could customize csrfTokenRequestHandler in HttpSecurity like this:

 1@Override
 2protected void configure(HttpSecurity http) throws Exception {
 3	// @formatter:off
 4    http
 5        // ... existing code here
 6        .csrf(c -> {
 7            c.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
 8            c.csrfTokenRequestHandler(new CsrfTokenRequestHandler());
 9        })
10        // ... existing code here
11    // @formatter:on
12}

Code above uses CsrfTokenRequestHandler to handle XSRF-TOKEN. But this solution fails on the first request and succeeds thereafter, and also, this means your app likely vulnerable against the BREACH attack.

There are special considerations for integrating a single-page application (SPA) with Spring Security’s CSRF protection.

Recall that Spring Security provides BREACH protection of the CsrfToken by default. When storing the expected CSRF token in a cookie, JavaScript applications will only have access to the plain token value and will not have access to the encoded value. A customized request handler for resolving the actual token value will need to be provided.

In addition, the cookie storing the CSRF token will be cleared upon authentication success and logout success. Spring Security defers loading a new CSRF token by default, and additional work is required to return a fresh cookie.

So, how to customize csrfTokenRequestHandler to prevent app from both CSRF and BREACH? The following configuration can be used:

 1@Configuration
 2@EnableWebSecurity
 3public class SecurityConfig {
 4
 5    @Bean
 6    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
 7        http
 8            // ...
 9            .csrf((csrf) -> csrf
10                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
11                .csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler())
12            );
13        return http.build();
14    }
15}
16
17final class SpaCsrfTokenRequestHandler implements CsrfTokenRequestHandler {
18    private final CsrfTokenRequestHandler plain = new CsrfTokenRequestAttributeHandler();
19    private final CsrfTokenRequestHandler xor = new XorCsrfTokenRequestAttributeHandler();
20
21    @Override
22    public void handle
23    (HttpServletRequest request, HttpServletResponse response, Supplier<CsrfToken> csrfToken) {
24        /*
25        * Always use XorCsrfTokenRequestAttributeHandler to provide BREACH protection of
26        * the CsrfToken when it is rendered in the response body.
27        */
28        this.xor.handle(request, response, csrfToken);
29        /*
30        * Render the token value to a cookie by causing the deferred token to be loaded.
31        */
32        csrfToken.get();
33    }
34
35    @Override
36    public String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) {
37        String headerValue = request.getHeader(csrfToken.getHeaderName());
38        /*
39        * If the request contains a request header, use CsrfTokenRequestAttributeHandler
40        * to resolve the CsrfToken. This applies when a single-page application includes
41        * the header value automatically, which was obtained via a cookie containing the
42        * raw CsrfToken.
43        *
44        * In all other cases (e.g. if the request contains a request parameter), use
45        * XorCsrfTokenRequestAttributeHandler to resolve the CsrfToken. This applies
46        * when a server-side rendered form includes the _csrf request parameter as a
47        * hidden input.
48        */
49        return (StringUtils.hasText(headerValue) ? this.plain : this.xor)
50            .resolveCsrfTokenValue(request, csrfToken);
51    }
52}
  • Configure CookieCsrfTokenRepository with HttpOnly set to false so the cookie can be read by the JavaScript application.
  • Configure a custom CsrfTokenRequestHandler that resolves the CSRF token based on whether it is an HTTP request header (X-XSRF-TOKEN) or request parameter (_csrf). This implementation also causes the deferred CsrfToken to be loaded on every request, which will return a new cookie if needed.

XorCsrfTokenRequestAttributeHandler resolves the CSRF TOKEN from request parameter _csrf.

Conclusion #

  • Spring Security 6 uses XorCsrfTokenRequestAttributeHandler to prevent CSRF and BREACH attacks.
  • Single page application which wants to prevent CSRF need to customize csrfTokenRequestHandler
  • You can use CookieCsrfTokenRepository to hold CSRF TOKEN, with HttpOnly set to false, the token can be read by Javascript.

References #