Use case
Signaling a CSRF problem to client app via 403 or some other status-code, while invalid sessions result in 401. For example in a SPA 401 might trigger re-authentication, while missing CSRF token might be an issue for a single request (which wouldn't need / want re-authentication).
Problem
I didn't find a "proper" way to handle / customize handling for missing CSRF token (MissingCsrfTokenException). I was expecting to get the error into AccessDeniedHandler specified in the ExceptionHandlingConfigurer.
I tracked the possible cause to CsrfConfigurer.createAccessDeniedHandler (code below) where a defined InvalidSessionStrategy results in instantiating a DelegatingAccessDeniedHandler with MissingCsrfTokenException mapped to an instantiated InvalidSessionAccessDeniedHandler, which in turn calls invalidSessionStrategy.onInvalidSessionDetected and drops the info on cause / exception. So a defined InvalidSessionStrategy takes precedence over an AccessDeniedHandler.
It surprised me a bit that a missing CSRF token leads to "invalid session detected".
private AccessDeniedHandler createAccessDeniedHandler(H http) {
InvalidSessionStrategy invalidSessionStrategy = getInvalidSessionStrategy(http);
AccessDeniedHandler defaultAccessDeniedHandler = getDefaultAccessDeniedHandler(http);
if (invalidSessionStrategy == null) {
return defaultAccessDeniedHandler;
}
InvalidSessionAccessDeniedHandler invalidSessionDeniedHandler = new InvalidSessionAccessDeniedHandler(
invalidSessionStrategy);
LinkedHashMap<Class<? extends AccessDeniedException>, AccessDeniedHandler> handlers = new LinkedHashMap<>();
handlers.put(MissingCsrfTokenException.class, invalidSessionDeniedHandler);
return new DelegatingAccessDeniedHandler(handlers, defaultAccessDeniedHandler);
}
https://github.com/spring-projects/spring-security/blob/371541a5cfee80e149d3a5308c63f0f81aeb6e2a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurer.java#LL311C2-L322C3
Possible solution
It would be nice to just handle the MissingCsrfTokenException directly via AccessDeniedHandler defined in ExceptionHandlingConfigurer (just like InvalidCsrfTokenException can be handled) or specify a custom AccessDeniedHandler for the CsrfConfigurer, which would take precedence over InvalidSessionStrategy.
Workarounds
Easy workaround is to have a custom CsrfTokenRequestAttributeHandler with resolveCsrfTokenValue returning String "null" instead of null-value CSRF token is missing, leading to InvalidCsrfTokenException instead of MissingCsrfTokenExeption.
It's also possible to handle the issue in the InvalidSessionStrategy.onInvalidSessionDetected by checking if the request should've had CSFR-token and whether it did, but this would start duplicating CSRF-logic there.
Comment From: marcusdacoregio
Hi @acutus, are you able to share your security configuration? Or, even better, a minimal reproducible sample to make that easier to debug on our side?
Comment From: spring-projects-issues
If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.
Comment From: acutus
Hi @marcusdacoregio,
I'm sorry, I don't seem to be able to find the time to create a minimal reproducible sample for this.
The point was mostly to bring up that in this case a defined InvalidSessionStrategy causes hard-coded handlers to be instantiated and document the workarounds.
Comment From: sjohnr
Hi @acutus, thanks for providing the details of your finding!
I took a look at this to understand it as you described, and found that the behavior provided in the framework is intentional, and further I feel it makes sense in this scenario. The MissingCsrfTokenException is passed to the accessDeniedHandler when there is no CSRF token available (in the session) to validate against. This indicates that either:
- this is the first request to the server that requires a CSRF token and no token previously existed, or
- the session expired
I think since 2 is much more likely than 1 for applications that correctly handle CSRF, it makes sense that this would result in an invalid session error. This behavior appears to have been in the framework since at least 2013. The general behavior is described on the CSRF page, though these specific details you outlined are not there.
However, I did notice that the docs are actually incorrect in this case. Currently, the docs state:
If a token does expire, you might want to customize how it is handled by specifying a custom
AccessDeniedHandler. The customAccessDeniedHandlercan process theInvalidCsrfTokenExceptionany way you like.
I think this should be changed to correctly state the behavior involving SessionConfigurer.invalidSessionStrategy, and the various options for configuring things to handle the MissingCsrfTokenException.
InvalidCsrfTokenException on the other hand seems straightforward, since you do in fact simply configure ExceptionHandlingConfigurer.accessDeniedHandler and it will be used for that exception.
Would you mind if I repurposed this ticket to fix the documentation to be correct and consistent with the details you provided above?
Comment From: acutus
Hi @sjohnr ,
Thanks, I don't mind at all, please go ahead. That's probably the best outcome if there's a documented way to handle the MissingCsrfTokenException properly.
And yes, the case in question is the number 1. in your comment, a single-page-app with multiple requests (including PUTs) firing directly at load after login (which invalidates the CSRF-token), so the 1st request might not have a valid token/cookie set. Of course it would be best to have the clients do proper CSRF-initialization, but due to backwards compatibility with multiple clients I still wanted to have the server to signal missing CSRF-token separately from otherwise invalid sessions (as that's how the older clients handle it, by retrying the request if server signals missing CSRF-token as they then should have the token set).
I think (didn't verify now) that Spring 5 gave 403 by default for missing CSRF-token but now that we are upgrading to Spring 6 and the new BREACH-protection requires defining a customized CsrfTokenRequestAttributeHandler for single page applications this behavior changed due to MissingCsrfTokenException resulting into 401 (via invalidSessionStrategy), which led me down this rabbit-hole :).
But like said, a documented way to handle the exception properly would be good solution.
Comment From: sjohnr
Ok, sounds good @acutus. Actually, I'm surprised you're noticing a difference in behavior, I didn't pick up on that in your description, perhaps I read it too fast. I'll look into that just to be sure I understand it.
Comment From: sjohnr
@acutus I double-checked the scenario I think you described. I want to remind you that the main behavior change to look out for with 6.x is that all dispatcher types pass through the AuthorizationFilter. So I believe the change in behavior you're describing is caused by the 403 error resulting in an ERROR dispatch, which the request (user) is not authorized for, resulting instead in a 401 error. This is assuming you have some extra things configured, such as a customized authenticationEntryPoint for the SPA.
You can fix this (for example) by adding this to your security config:
http
.authorizeHttpRequests((authorize) -> authorize
.dispatcherTypeMatchers(DispatcherType.ERROR).permitAll()
// ...
.anyRequest().authenticated()
)
Note that the behavior around MissingCsrfTokenException did not change, and I'm still a bit confused as to why you would see differences between the two versions in the case where you've specified a custom InvalidSessionStrategy. It seems that in your case, you shouldn't have a configured invalidSessionStrategy. If you do need that for a specific type of client (e.g. a non-SPA browser-based application?), I think you have two options.
- Set up different filter chains, one for each type of client and only configure
invalidSessionStrategyfor the one where it makes sense. - Use an
ObjectPostProcessorto override theaccessDeniedHandlerused by theCsrfFilter.
This, I think, is the information we could put in the docs. It's also possible it makes sense to enhance CsrfConfigurer to allow customizing accessDeniedHandler.