20

I upgraded my project to Spring Boot 3 and Spring Security 6, but since the upgrade the CSRF protection is no longer working.

I'm using the following configuration:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    return http
        .authorizeHttpRequests(authorize -> authorize
            .anyRequest().authenticated())
        .httpBasic(withDefaults())
        .sessionManagement(session -> session
            .sessionCreationPolicy(SessionCreationPolicy.ALWAYS))
        .csrf(csrf -> csrf
            .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))
        .build();
}

@Bean
public UserDetailsService userDetailsService() {
     UserDetails user = User.builder().username("user").password("{noop}test").authorities("user").build();
     return new InMemoryUserDetailsManager(user);
}

On my webpage I only have a single button:

<button id="test">Test CSRF</button>

And the following JavaScript code:

document.querySelector("#test").addEventListener('click', async function() {
  console.log('Clicked');
  // This code reads the cookie from the browser
  // Source: https://stackoverflow.com/a/25490531
  const csrfToken = document.cookie.match('(^|;)\\s*XSRF-TOKEN\\s*=\\s*([^;]+)')?.pop();
  const result = await fetch('./api/foo', {
    method: 'POST',
    headers: {
      'X-XSRF-Token': csrfToken
    }
  });
  console.log(result);
});

In Spring Boot 2.7.x this setup works fine, but if I upgrade my project to Spring Boot 3 and Spring Security 6, I get a 403 error with the following debug logs:

15:10:51.858 D         o.s.security.web.csrf.CsrfFilter: Invalid CSRF token found for http://localhost:8080/api/foo
15:10:51.859 D   o.s.s.w.access.AccessDeniedHandlerImpl: Responding with 403 status code

My guess is that this is related to the changes for #4001. However I don't understand what I have to change to my code or if I have to XOR something.

I did check if it was due to the new deferred loading of the CSRF token, but even if I click the button a second time (and verifying that the XSRF-TOKEN cookie is set), it still doesn't work.

g00glen00b
  • 41,995
  • 13
  • 95
  • 133
  • You must ensure that the webpage with the `test` button on it is loaded from the server each time with a fresh `XSRF-TOKEN` cookie, and is not taken from the browser cache (with an expired cookie). Check the network tab in the browser console whether this is the case. – Heiko Theißen Nov 19 '22 at 14:19
  • 1
    @HeikoTheißen I did that. From what I can see during debugging is that the new XOR CSRF request handler in Spring Security expects an XOR'ed CSRF token. But on the other hand, the cookie CSRF repository doesn't return an XOR'ed CSRF token but a normal one. So when I debug the CSRF handler, I see that they check the byte length of the two tokens (the expected one and the one passed as a header) and they don't match so the handler returns `null` and the call fails. – g00glen00b Nov 19 '22 at 15:52
  • (That's also the reason why it works again if I change the CSRF handler back to the original one, as seen in the answers by both myself and Matt.) – g00glen00b Nov 19 '22 at 15:54
  • I have the same problem in my app, the logout doesn't work for the same reason XSRF-TOKEN is absent from the cookie. I added this configuration `CsrfTokenRequestAttributeHandler requestHandler = new CsrfTokenRequestAttributeHandler(); requestHandler.setCsrfRequestAttributeName(null); http.csrf() .csrfTokenRepository(getCookieCsrfTokenRepository()) .csrfTokenRequestHandler(requestHandler)` Now the token exists in all request cookies, but the problem is that **X-XSS-Protection** is 0 , but should be **1; mode=block** Do you have a solution for that? – user1187329 Jan 23 '23 at 08:43

9 Answers9

20

I have recently added a section to the reference documentation for migrating to 5.8 (in preparation to 6.0) that demonstrates a solution for this issue.

TL;DR See I am using AngularJS or another Javascript framework.

The issue here is that AngularJS (and your example code above) are using the XSRF-TOKEN cookie directly. Prior to Spring Security 6, this was fine. But unfortunately, the cookie is actually used to persist the raw token, and with Spring Security 6, the raw token is not accepted by default. Ideally, front-end frameworks would be able to use another source to get the token, such as an X-XSRF-TOKEN response header.

However, even with Spring Security 6, such a response header is not provided out of the box, though it could be a possible enhancement worth suggesting. I have not yet suggested such an enhancement since Javascript frameworks would not be able to use it by default.

For now, you will need to work around the problem by configuring Spring Security 6 to accept raw tokens, as suggested in the section I linked above. The suggestion allows raw tokens to be submitted, but continues to use the XorCsrfTokenRequestAttributeHandler to make available the hashed version of the request attribute (e.g. request.getAttribute(CsrfToken.class.getName()) or request.getAttribute("_csrf")), in case anything renders the CSRF token to an HTML response which could be vulnerable to BREACH.

I would recommend finding a reputable source for researching BREACH more thoroughly, but unfortunately I cannot claim to be such a source.

I would also recommend keeping an eye on Spring Security issues for now, as things may change quickly once the community begins consuming Spring Security 6. You can use this filter as a possible way to keep track of CSRF-related issues.

Steve Riesenberg
  • 4,271
  • 1
  • 4
  • 26
  • Thanks for the response, but I fail to understand the implications. Given that the CSRF Token is never rendered to a HTML page it seems like I don't really need the new implementation anyway, right? Personally I don't get the feature as a whole. How can this "per request" CSRF Token even be generated in a world of multiple parallel calls? It's not like you can create a new one once the old one is handed in, since already running parallel requests can not now about it. – Mario B Nov 27 '22 at 08:09
  • @MarioB, the underlying csrf token does not change per request, but is encoded with random bytes so it can provide out-of-the-box protection for BREACH. If you don't require this protection, you can certainly revert to normal CSRF protection using `CsrfTokenRequestAttributeHandler`. – Steve Riesenberg Nov 28 '22 at 14:55
  • Was there a change about the creation of csrf token too? After upgrading, I now see the first protected request failing, only during the failed request the csrf token is created and subsequent requests succeed. – Felix Jan 05 '23 at 01:52
  • Sorry @Felix, I think I'd need more details to fully answer your question. Mind opening a new SO question? – Steve Riesenberg Jan 05 '23 at 15:46
  • @Felix My guess is that it's due to the deferred loading of the CSRF token. Check the chapter before the BREACH attack chapter in the documentation linked by Steve. – g00glen00b Jan 05 '23 at 23:34
  • @g00glen00b thanks, that’s a good point. I will check that during the weekend. If it doesn’t work i will open a new question – Felix Jan 06 '23 at 20:49
  • I can confirm my setup works with the example to go back to 5.8 behavior as linked in the documentation linked by Steve. This is likely required because I use a special setup for authentication, which uses no sessions (`SessionCreationPolicy.STATELESS`). Every request is authenticated through a custom `SecurityContextRepository` and no sessions are created. – Felix Jan 10 '23 at 00:04
  • I added this CsrfTokenRequestAttributeHandler to my security configuration, but I still have issues with CSRF. I would appriciate if you take a look https://stackoverflow.com/questions/75080739/spring-security-6-post-requests-are-unauthorised-with-permitall/75083797?noredirect=1#comment132518265_75083797 – Octavia Jan 12 '23 at 09:35
  • @SteveRiesenberg I read the reference documentation like 10 times, but I am still not sure. It seems to say (in the NOTE box) that BREACH protection is only necessary if I include the token in the response body. Reversely, this seems to say that if I DO NOT return the token in the body (it's transferred as a cookie) that protection is meaningless and safe to be deactivated. Correct? – Mario B Mar 02 '23 at 09:31
  • @MarioB I apologize that it was not clear to you. Please don't hesitate to open an issue with any suggested improvements. Regarding your question, that is not exactly what I was trying to convey with the NOTE under "I am using AngularJS or another Javascript framework." Above the NOTE it says "This is the RECOMMENDED way...". The NOTE intends to let you know that the protection is there for anything that includes the token in the response body (such as html responses). This is because you may be returning HTML somewhere and not know it. Disable BREACH only if you know what you're doing. – Steve Riesenberg Mar 02 '23 at 17:40
13

We have an angular angular application with spring-boot. We tried to migrate to spring-boot 3 (Spring Security 6). And we faced the same problem.

We tried many methods including some of the solutions from this question's answer but we failed. After spending time we found the solution from the spring security doc.

What we need to do is, set the CsrfRequestAttributeName to null in the configuration.

requestHandler.setCsrfRequestAttributeName(null);

What actually happened:
The CsrfToken will be loaded on each request in Spring Security version 5 by default. This means that in a typical setup, every request—even those that are not necessary—must have the HttpSession read.

The default behavior of Spring Security 6 is to postpone looking up the CsrfToken until it is required.

Our application needs the token every time. So, We need to opt into the 5.8 defaults.

The example code is given below (from doc):

@Bean
DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
    CsrfTokenRequestAttributeHandler requestHandler = new CsrfTokenRequestAttributeHandler();
    // set the name of the attribute the CsrfToken will be populated on
    requestHandler.setCsrfRequestAttributeName(null);
    http
        // ...
        .csrf((csrf) -> csrf
            .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
            .csrfTokenRequestHandler(requestHandler)
        );
    return http.build();
}
  • As far as I understand this solution, it is not using `XorCsrfTokenRequestAttributeHandler` and, as so, is exposed to BREACH. What about using `new XorCsrfTokenRequestAttributeHandler()` and then `csrfTokenRequestHandler(requestHandler::handle)` as instructed in the doc linked in the accepted answer? – ch4mp Mar 12 '23 at 23:34
2

I currently worked around the problem by disabling the XorCsrfTokenRequestAttributeHandler like this:

.csrf(csrf -> csrf
    .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
    // Added this:
    .csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler()))

However, this means that I'm likely vulnerable against the BREACH attack.

g00glen00b
  • 41,995
  • 13
  • 95
  • 133
  • Yes, you are. The accepted answer (posted after yours) references a solution protecting against both CSRF and BREACH for servlets (like your app is, obviously) and I just posted the equivalent for reactive apps. – ch4mp Mar 12 '23 at 23:29
2

Thanks for this! I was able to use it to solve a similar project in a JHipster + Spring Boot 3 app. However, it seems the class name might've changed recently. Here's what I had to use:

.csrf(csrf -> csrf
    .csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse())
    .csrfTokenRequestHandler(new ServerCsrfTokenRequestAttributeHandler()))
Matt Raible
  • 8,187
  • 9
  • 61
  • 120
  • Thanks for the information. I only took a quick look but that appears to be the reactive counterpart to the `CsrfTokenRequestAttributeHandler` class. I'm just not very experienced in security-stuff but I guess it's less secure than `XorCsrfTokenRequestAttributeHandler ` (or the reactive `XorServerCsrfTokenRequestAttributeHandler` counterpart) so I wonder if there's a solution that utilizes the XOR-logic. – g00glen00b Nov 18 '22 at 21:12
  • This is actually disabling the `XorCsrfTokenRequestAttributeHandler` and exposing to BREACH attacks. – ch4mp Mar 12 '23 at 23:00
2

As of Spring Security 6.0.1 and Spring Boot 3.0.2, following the instructions from the accepted answer fails on the first request but succeeds thereafter. The reason it fails on the first request is because the token's cookie never gets created until a protected method is invoked. This is because the method CookieCsrfTokenRepository.saveToken only gets called when the CsrfFilter calls deferredCsrfToken.get(), which only gets called on POST, PUT, PATCH, and DELETE methods. Unfortunately, under the current implementation, that means the client has to expect a failure on the first request. Under previous versions of Spring Security, we used to be able to count on the token's cookie being included in the response to GET, HEAD, or OPTIONS requests.

rptmaestro
  • 21
  • 4
2

The documentation pointed by @steve-reisenberg is adapted to servlets.

Here is the adaptation for webflux apps (like spring-cloud-gateway):

http.csrf((csrf) -> csrf
        .csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse())
        .csrfTokenRequestHandler(new XorServerCsrfTokenRequestAttributeHandler()::handle));

This should protect against both CSRF and BREACH (as opposed to the answers referencing (Server)CsrfTokenRequestAttributeHandler which are exposed to BREACH).

ch4mp
  • 6,622
  • 6
  • 29
  • 49
1

Using the accepted answer breaks tests that require CSRF using Spring Security's SecurityMockMvcRequestPostProcessors.crsf() I can either only use CsrfTokenRequestAttributeHandler, or XorCsrfTokenRequestAttributeHandler in Spring Boot's CSRF configuration, both give positive test results.

Using the accepted answer makes Angular work but breaks tests.

So the only workaround at the moment seems to be using CsrfTokenRequestAttributeHandler and so effectively disabling Spring Security's BREACH-protection.

0

I have created an issue for a different scenario where you need to send the CSRF token with a header through JavaScript which wasn't clear in the documentation. If you have a multi-page app like one where you mostly mount React components inside the HTML, it might be useful for you.

Basically what you need is to use X-CSRF-TOKEN header using the default CSRF configuration.

https://github.com/spring-projects/spring-security/issues/13009

Cagatay Kalan
  • 4,066
  • 1
  • 30
  • 23
0

This is what it works for me

CookieCsrfTokenRepository tokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse();
XorCsrfTokenRequestAttributeHandler delegate = new XorCsrfTokenRequestAttributeHandler();
// set the name of the attribute the CsrfToken will be populated on
delegate.setCsrfRequestAttributeName(null);
// Use only the handle() method of XorCsrfTokenRequestAttributeHandler and the
// default implementation of resolveCsrfTokenValue() from CsrfTokenRequestHandler
CsrfTokenRequestHandler requestHandler = delegate::handle;

...

.csrf((csrf) -> csrf
   .csrfTokenRepository(tokenRepository)
   .csrfTokenRequestHandler(requestHandler)
)
alikian
  • 21
  • 7