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 ofCsrfTokenRepository
, 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 usewithHttpOnlyFalse()
.So what does attribute
HttpOnly
mean? This is an attribute of Cookie, onceHttpOnly
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
withHttpOnly
set tofalse
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, withHttpOnly
set to false, the token can be read by Javascript.