Dimitri Mestdagh
Dimitri Mestdagh

Reputation: 44745

CSRF protection not working with Spring Security 6

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.

Upvotes: 39

Views: 42093

Answers (10)

Steve Riesenberg
Steve Riesenberg

Reputation: 6168

I added a section to the reference documentation for migrating to 5.8 (in preparation to 6.0) that demonstrates a solution for this issue, which has since been updated in later versions of the documentation.

TL;DR See CSRF with JavaScript Applications

The issue here is that Angular (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.

Update: This article is one possible starting point.

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.

Upvotes: 40

Hashcon
Hashcon

Reputation: 595

Here is an solution for postman test script if you want to use spring security 6 default csrf handler (XorCsrfTokenRequestAttributeHandler):

const createXoredCsrfToken = (token) => {
  const tokenBytes = [];
  for (let i = 0; i < token.length; i++) {
    tokenBytes.push(token.charCodeAt(i));
  }

  const randomBytes = new Uint8Array(tokenBytes.length);
  for (let i = 0; i < randomBytes.length; i++) {
    randomBytes[i] = Math.floor(Math.random() * 256);
  }

  const xoredBytes = xorCsrf(randomBytes, tokenBytes);
  const combinedBytes = new Uint8Array(tokenBytes.length + randomBytes.length);
  combinedBytes.set(randomBytes);
  combinedBytes.set(xoredBytes, randomBytes.length);

  return btoa(String.fromCharCode.apply(null, combinedBytes))
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=+$/, '');
};

const xorCsrf = (randomBytes, csrfBytes) => {
  if (csrfBytes.length < randomBytes.length) {
    return null;
  }
  const len = Math.min(randomBytes.length, csrfBytes.length);
  const xoredCsrf = new Uint8Array(len);
  xoredCsrf.set(csrfBytes.slice(0, len));
  for (let i = 0; i < len; i++) {
    xoredCsrf[i] ^= randomBytes[i];
  }
  return xoredCsrf;
};

//Replace XSFR-TOKEN with your cookie name
var xsrfCookie = postman.getResponseCookie("XSRF-TOKEN");
postman.setEnvironmentVariable("xsrf-token", createXoredCsrfToken(xsrfCookie.value));


enter image description here

Upvotes: 0

ch4mp
ch4mp

Reputation: 12899

The documentation pointed by @steve-reisenberg is adapted to servlets (and is now outdated, the new version is there)

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

http.csrf(csrf -> csrf
    .csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse())
    .csrfTokenRequestHandler(new SpaServerCsrfTokenRequestHandler()));

with:

/**
 * Adapted from https://docs.spring.io/spring-security/reference/servlet/exploits/csrf.html#csrf-integration-javascript-spa
 */
static final class SpaServerCsrfTokenRequestHandler extends ServerCsrfTokenRequestAttributeHandler {
    private final ServerCsrfTokenRequestAttributeHandler delegate = new XorServerCsrfTokenRequestAttributeHandler();

    @Override
    public void handle(ServerWebExchange exchange, Mono<CsrfToken> csrfToken) {
        /*
         * Always use XorCsrfTokenRequestAttributeHandler to provide BREACH protection of the CsrfToken when it is rendered in the response body.
         */
        this.delegate.handle(exchange, csrfToken);
    }

    @Override
    public Mono<String> resolveCsrfTokenValue(ServerWebExchange exchange, CsrfToken csrfToken) {
        final var hasHeader = exchange.getRequest().getHeaders().get(csrfToken.getHeaderName()).stream().filter(StringUtils::hasText).count() > 0;
        return hasHeader ? super.resolveCsrfTokenValue(exchange, csrfToken) : this.delegate.resolveCsrfTokenValue(exchange, csrfToken);
    }
}

@Bean
WebFilter csrfCookieWebFilter() {
    return (exchange, chain) -> {
        exchange.getAttributeOrDefault(CsrfToken.class.getName(), Mono.empty()).subscribe();
        return chain.filter(exchange);
    };
}

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

Upvotes: 9

alikian
alikian

Reputation: 19

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

Upvotes: -1

Cagatay Kalan
Cagatay Kalan

Reputation: 4126

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

Upvotes: 0

rptmaestro
rptmaestro

Reputation: 71

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.

Upvotes: 7

Abdullah AL Habib
Abdullah AL Habib

Reputation: 378

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

Upvotes: 27

Jan Kohnert
Jan Kohnert

Reputation: 51

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.

Upvotes: 1

Matt Raible
Matt Raible

Reputation: 8644

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()))

Upvotes: 3

Dimitri Mestdagh
Dimitri Mestdagh

Reputation: 44745

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.

Upvotes: 2

Related Questions