Reputation: 44745
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
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
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));
Upvotes: 0
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
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
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
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
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
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
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
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