Skip to content

Fix CSRF protection provided by @EnableWebSocketSecurity / Stomp #12378

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
emopti-jrufer opened this issue Dec 14, 2022 · 31 comments
Closed

Fix CSRF protection provided by @EnableWebSocketSecurity / Stomp #12378

emopti-jrufer opened this issue Dec 14, 2022 · 31 comments
Assignees
Labels
in: messaging An issue in spring-security-messaging type: bug A general bug
Milestone

Comments

@emopti-jrufer
Copy link
Contributor

CSRF protection provided by @EnableWebSocketSecurity is broken. I have identified 2 things that prevent the CsrfChannelInterceptor from validating the CSRF token from the CONNECT headers.

First problem encounted was the stack trace below. The CsrfChannelInterceptor was trying to access the CsrfToken when processing the CONNECT message. The changes to made in 475b3bb and d94677f to defer the loading of the token caused the deferred supplier to try to access the http upgrade HttpServletRequest after it has gone out of scope.

Attempting to come up with a fix for the first problem I added an additional HandshakeInterceptor that forced eager loading the CsrfToken while they upgrade HttpServeletRequest was still in scope. I added the source below for clarity.

This overcame the first problem but then the CsrfChannelInterceptor started throwing InvalidCsrfTokenException. After investigating it became apparent the CsrfChannelInterceptor was only design to work with tokens that were not masked. Since XorCsrfTokenRequestAttributeHandler is now the default implementation for CsrfTokenRequestHandler the CsrfChannelInterceptor needs to be modified to have the same or similar logic as the CsrfFilter#doFilterInternal.

 stack_trace: org.springframework.messaging.MessageDeliveryException: Failed to send message to ExecutorSubscribableChannel[clientInboundChannel]
	at org.springframework.messaging.support.AbstractMessageChannel.send(AbstractMessageChannel.java:149)
	at org.springframework.messaging.support.AbstractMessageChannel.send(AbstractMessageChannel.java:125)
	at org.springframework.web.socket.messaging.StompSubProtocolHandler.handleMessageFromClient(StompSubProtocolHandler.java:310)
	at org.springframework.web.socket.messaging.SubProtocolWebSocketHandler.handleMessage(SubProtocolWebSocketHandler.java:335)
	at org.springframework.web.socket.handler.WebSocketHandlerDecorator.handleMessage(WebSocketHandlerDecorator.java:75)
	at org.springframework.web.socket.handler.WebSocketHandlerDecorator.handleMessage(WebSocketHandlerDecorator.java:75)
	at org.springframework.web.socket.handler.WebSocketHandlerDecorator.handleMessage(WebSocketHandlerDecorator.java:75)
	at org.springframework.web.socket.handler.LoggingWebSocketHandlerDecorator.handleMessage(LoggingWebSocketHandlerDecorator.java:56)
	at org.springframework.web.socket.handler.ExceptionWebSocketHandlerDecorator.handleMessage(ExceptionWebSocketHandlerDecorator.java:58)
	at org.springframework.web.socket.adapter.standard.StandardWebSocketHandlerAdapter.handleTextMessage(StandardWebSocketHandlerAdapter.java:113)
	at org.springframework.web.socket.adapter.standard.StandardWebSocketHandlerAdapter$3.onMessage(StandardWebSocketHandlerAdapter.java:84)
	at org.springframework.web.socket.adapter.standard.StandardWebSocketHandlerAdapter$3.onMessage(StandardWebSocketHandlerAdapter.java:81)
	at org.apache.tomcat.websocket.WsFrameBase.sendMessageText(WsFrameBase.java:415)
	at org.apache.tomcat.websocket.server.WsFrameServer.sendMessageText(WsFrameServer.java:129)
	at org.apache.tomcat.websocket.WsFrameBase.processDataText(WsFrameBase.java:515)
	at org.apache.tomcat.websocket.WsFrameBase.processData(WsFrameBase.java:301)
	at org.apache.tomcat.websocket.WsFrameBase.processInputBuffer(WsFrameBase.java:133)
	at org.apache.tomcat.websocket.server.WsFrameServer.onDataAvailable(WsFrameServer.java:85)
	at org.apache.tomcat.websocket.server.WsFrameServer.doOnDataAvailable(WsFrameServer.java:183)
	at org.apache.tomcat.websocket.server.WsFrameServer.notifyDataAvailable(WsFrameServer.java:162)
	at org.apache.tomcat.websocket.server.WsHttpUpgradeHandler.upgradeDispatch(WsHttpUpgradeHandler.java:157)
	at org.apache.coyote.http11.upgrade.UpgradeProcessorInternal.dispatch(UpgradeProcessorInternal.java:60)
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:59)
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:861)
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1739)
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)
	at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)
	at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
	at java.base/java.lang.Thread.run(Thread.java:833)
Caused by: java.lang.IllegalStateException: The request object has been recycled and is no longer associated with this facade
	at org.apache.catalina.connector.RequestFacade.getAttribute(RequestFacade.java:279)
	at jakarta.servlet.ServletRequestWrapper.getAttribute(ServletRequestWrapper.java:85)
	at org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper.getCurrentSession(SessionRepositoryFilter.java:238)
	at org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper.getSession(SessionRepositoryFilter.java:281)
	at org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper.getSession(SessionRepositoryFilter.java:194)
	at jakarta.servlet.http.HttpServletRequestWrapper.getSession(HttpServletRequestWrapper.java:244)
	at jakarta.servlet.http.HttpServletRequestWrapper.getSession(HttpServletRequestWrapper.java:244)
	at org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository.loadToken(HttpSessionCsrfTokenRepository.java:65)
	at org.springframework.security.web.csrf.RepositoryDeferredCsrfToken.init(RepositoryDeferredCsrfToken.java:63)
	at org.springframework.security.web.csrf.RepositoryDeferredCsrfToken.get(RepositoryDeferredCsrfToken.java:48)
	at org.springframework.security.web.csrf.XorCsrfTokenRequestAttributeHandler.lambda$deferCsrfTokenUpdate$0(XorCsrfTokenRequestAttributeHandler.java:63)
	at org.springframework.security.web.csrf.XorCsrfTokenRequestAttributeHandler$CachedCsrfTokenSupplier.get(XorCsrfTokenRequestAttributeHandler.java:139)
	at org.springframework.security.web.csrf.XorCsrfTokenRequestAttributeHandler$CachedCsrfTokenSupplier.get(XorCsrfTokenRequestAttributeHandler.java:126)
	at org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler$SupplierCsrfToken.getDelegate(CsrfTokenRequestAttributeHandler.java:89)
	at org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler$SupplierCsrfToken.getHeaderName(CsrfTokenRequestAttributeHandler.java:75)
	at org.springframework.security.messaging.web.csrf.CsrfChannelInterceptor.preSend(CsrfChannelInterceptor.java:56)
	at org.springframework.messaging.support.AbstractMessageChannel$ChannelInterceptorChain.applyPreSend(AbstractMessageChannel.java:181)
	at org.springframework.messaging.support.AbstractMessageChannel.send(AbstractMessageChannel.java:135)
	... 30 common frames omitted

public class CsrfTokenHandshakeInterceptorFix implements HandshakeInterceptor {
    @Override
    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
        HttpServletRequest httpRequest = ((ServletServerHttpRequest) request).getServletRequest();
        CsrfToken token = (CsrfToken) httpRequest.getAttribute(CsrfToken.class.getName());

        if (token == null) {
            return true;
        }

        // Eagerly resolve token values
        token = new DefaultCsrfToken(token.getHeaderName(), token.getParameterName(), token.getToken());
        attributes.put(CsrfToken.class.getName(), token);
        return true;
    }

    @Override
    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
    }
})
@emopti-jrufer emopti-jrufer added status: waiting-for-triage An issue we've not yet triaged type: bug A general bug labels Dec 14, 2022
@sjohnr
Copy link
Contributor

sjohnr commented Dec 15, 2022

Hi @emopti-jrufer! Thanks for reporting this issue.

Would you be able to put together a minimal, reproducible sample with a unit test that demonstrates the behavior? Ideally any external dependencies (if any) would be mocked, but I'm not personally familiar with how to do so. If it's not possible to put together a sample, it may be a bit of time before I could take a look at this.

@sjohnr sjohnr added status: waiting-for-feedback We need additional information before we can continue and removed status: waiting-for-triage An issue we've not yet triaged labels Dec 15, 2022
@emopti-jrufer
Copy link
Contributor Author

@sjohnr I created a sample that reproduces the issues. https://github.com/emopti-jrufer/spring-security-issue-12378-sample.

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Dec 16, 2022
@OlliL
Copy link

OlliL commented Jan 4, 2023

I faced the exact same Issue. AbstractSecurityWebSocketMessageBrokerConfigurer is deprecated so I went with @EnableWebSocketSecurity. I also have a custom ChannelInterceptor in place which checks the JWT Bearer token I provide with the CONNECT call.
Unfortunally it is no longer possible to disable csrf with STOMP as the new Annotation leaves no option for that. So I went and implemented CSRF and just found out the same issue @emopti-jrufer found. It took me days and I finally ended up creating a clean room example last weekend with JWT checking + CSRF to show this error - just to find out an issue has already been opened.

Please work on that asap as basically STOMP Websockets are broken and can't be used.

The 2 fixes (CsrfTokenHandshakeInterceptorFix, exchanging csrfTokenRequestHandler) @emopti-jrufer provided in his example implementation fixed the issue for me as a workaround.

@sjohnr sjohnr added in: messaging An issue in spring-security-messaging and removed status: feedback-provided Feedback has been provided labels Jan 4, 2023
@sjohnr
Copy link
Contributor

sjohnr commented Jan 4, 2023

Thanks @emopti-jrufer for the sample! I was able to downgrade your sample to Spring Security 5.8.1 so I can do some testing prior to upgrading to 6.0 and can definitely see the issue. I unfortunately am not very familiar with websocket/stomp support so I missed this.

@OlliL sorry to hear about your troubles. Thanks for verifying the workaround!

Based on my testing, I believe the workaround can be simplified to no longer need the EagerCsrfTokenHandshakeInterceptor from your sample by using the configuration in the 5.8 migration guide under Explicit Configure CsrfToken with 5.8 Defaults. It would look like this:

@Configuration
@EnableWebSecurity
public class SecurityConfiguration {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		CsrfTokenRequestAttributeHandler requestHandler = new CsrfTokenRequestAttributeHandler();
		requestHandler.setCsrfRequestAttributeName(null); // <-- opt out of deferred tokens in 6.0
		http
			.authorizeHttpRequests((authorize) -> authorize
				.requestMatchers("/csrf").permitAll()
				.anyRequest().authenticated()
			)
			.csrf((csrf) -> csrf
				.csrfTokenRequestHandler(requestHandler)
			)
			.formLogin((formLogin) -> formLogin
				.successHandler(new BasicAuthenticationSuccessHandler())
			);

		return http.build();
	}

	// ...

}

@emopti-jrufer can you verify that the above configuration removes the need for a custom HandshakeInterceptor in your case?

This one looks like a tricky issue to resolve given the 5.8/6.0 work for CsrfFilter involved introducing new APIs and the messaging layer adds another level of abstraction. I'll let you know when I have an update on this.

@kaans
Copy link

kaans commented Jan 5, 2023

I did face the same problem and struggled for some days, thanks @emopti-jrufer for your initial solution! It did work for me as well.

I just tried the solution of @sjohnr removing the custom HandshakeInterceptor and opting out of deferred tokens. It also worked for me, thank you. I'd appreciate an update if the workarounds are not needed anymore.

@emopti-jrufer
Copy link
Contributor Author

emopti-jrufer commented Jan 5, 2023

@sjohnr the solution you provided from a code point is simpler to implement but there is a small downside where the HTTP upgrade HttpServletRequest and HttpServletResponse objects contained in the deferred variants of the CsrfToken will never get released until the WebSocket is closed.

It should be noted that the simpler solution does not stop using deferred tokens but rather triggers them to be resolved right away.

@OlliL
Copy link

OlliL commented Jan 5, 2023

I can also confirm that its working with the remark @emopti-jrufer explained.... Since I already created an example to show the issue I link it here: https://github.com/OlliL/spring-boot-stomp - the fixes are already applied. There is a simple HTML Page served which opens a Websocket-Connection to the running Spring Boot Container to show the issue.... I know its no longer needed but the code was already created by me :)

@sjohnr
Copy link
Contributor

sjohnr commented Jan 5, 2023

there is a small downside where the HTTP upgrade HttpServletRequest and HttpServletResponse objects contained in the deferred variants of the CsrfToken will never get released until the WebSocket is closed.

That is a great point, @emopti-jrufer, thanks for pointing that out!

created an example to show the issue

Thanks @OlliL!

Thanks for verifying @kaans!

@sjohnr sjohnr added this to the 5.8.2 milestone Jan 9, 2023
@jzheaux jzheaux self-assigned this Jan 10, 2023
@sjohnr
Copy link
Contributor

sjohnr commented Jan 17, 2023

@emopti-jrufer (and others) thanks for your patience!

I have created a branch on 5.8.x with a tentative fix for this issue. I have also created a fork of your sample on 5.8.2-SNAPSHOT dependencies. On my branch, you can do the following (see full example):

    @Bean
    public ChannelInterceptor csrfChannelInterceptor() {
        return new XorCsrfChannelInterceptor();
    }

Would you have a few minutes to try this out in a real application? You can check out my branch and install snapshots to maven local as follows[1]:

  • git clone [email protected]:sjohnr/spring-security.git && cd spring-security
  • git checkout gh-12378-csrf-websocket-stomp
  • sdk env (assumes you have SDKMAN! installed)
  • ./gradlew -xtest publishToMavenLocal

[1]: On Mac/Linux. Steps for Windows should be similar.

If a 6.0 baseline would be better for you, let me know and I can prepare a branch.

@emopti-jrufer
Copy link
Contributor Author

@sjohnr
A 6.0 baseline would be the quickest way for me to test this change.

@sjohnr
Copy link
Contributor

sjohnr commented Jan 17, 2023

Ok @emopti-jrufer no problem. The reason I mention it is that we recommend upgrading to Spring Security 5.8 first using the 5.8 migration guide.

Branch of 6.0.x: gh-12378-6.0.x

Following similar steps as above will (currently) build 6.0.2-SNAPSHOT.

@emopti-jrufer
Copy link
Contributor Author

@sjohnr
I have tested gh-12378-6.0.x in our application with no obvious issues.

I am assuming you will be setting XorCsrfChannelInterceptor as the default csrfChannelInterceptor for the 6.x code line since the default CsrfTokenRequestHandler is XorCsrfTokenRequestAttributeHandler. This would negate the need for adding the csrfChannelInterceptor bean.

@sjohnr
Copy link
Contributor

sjohnr commented Jan 18, 2023

I am assuming you will be setting XorCsrfChannelInterceptor as the default csrfChannelInterceptor for the 6.x code line since the default CsrfTokenRequestHandler is XorCsrfTokenRequestAttributeHandler.

That's correct! And good point, I did not do that on this branch but will do so before merging. Would you like me to apply that change so you can test once more, or are you good with the testing as is?

@emopti-jrufer
Copy link
Contributor Author

I am good as is.

@sjohnr sjohnr changed the title CSRF protection provided by @EnableWebSocketSecurity / Stomp Fix CSRF protection provided by @EnableWebSocketSecurity / Stomp Jan 23, 2023
@sjohnr sjohnr modified the milestones: 5.8.2, 6.0.2 Jan 23, 2023
@sjohnr sjohnr removed a link to a pull request Jan 23, 2023
sjohnr pushed a commit to sjohnr/spring-security that referenced this issue Jan 26, 2023
sjohnr pushed a commit to sjohnr/spring-security that referenced this issue Jan 26, 2023
sjohnr pushed a commit that referenced this issue Jan 26, 2023
@sjohnr sjohnr closed this as completed in 13487be Jan 26, 2023
@OlliL
Copy link

OlliL commented Feb 26, 2023

Uhm.... so I updated to 6.0.2 and removed the EagerCsrfTokenHandshakeInterceptor from my StompEndpoint registry as well as my csrfTokenRequestHandler from my csrf configuration. But even if I didn't - CSRF stopped working....

grafik

Judging from XorCsrfTokenUtils.getTokenValue It feels like there is some base64 encoding expected?! I wonder what am I missing

CSRF Config:

        .csrf(configurer -> {
          // FIX for https://github.com/spring-projects/spring-security/issues/12378
          configurer.csrfTokenRequestHandler(new XorCsrfTokenRequestAttributeHandler());
          configurer.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
          configurer.ignoringRequestMatchers(OPEN_ENDPOINTS);
        })

Websocket Config:

  @Override
  public void registerStompEndpoints(final StompEndpointRegistry registry) {
    // FIX for https://github.com/spring-projects/spring-security/issues/12378
    registry.addEndpoint("/websocket") // .addInterceptors(new EagerCsrfTokenHandshakeInterceptor())
        .setAllowedOrigins("*");
  }

Edit

I currently have no idea why the server does not emit an Xored CSRF Token but expects one back - could it be an issue with the CookieCsrfTokenRepository I'm using?

So right now I had to explicitly define that bean to get it working -

  @Bean(name = "csrfChannelInterceptor")
  public ChannelInterceptor csrfChannelInterceptor() {
    return new CsrfChannelInterceptor();
  }

Any hint what could have caused this issue would be appreciated.

@sjohnr
Copy link
Contributor

sjohnr commented Feb 27, 2023

@OlliL, I've updated the migration guide for 5.8 to include the upgrade steps to prepare for 6.0. I also added cleanup steps to the 6.0 migration guide. You should no longer need the workaround with EagerCsrfTokenHandshakeInterceptor and can remove the @Bean for CsrfChannelInterceptor as well.

I currently have no idea why the server does not emit an Xored CSRF Token but expects one back - could it be an issue with the CookieCsrfTokenRepository I'm using?

In your case, it sounds like you would also benefit from reviewing the info in the 5.8 migration guide on using CookieCsrfTokenRepository. This outlines how to deal with deferred CSRF tokens.

@OlliL
Copy link

OlliL commented Feb 27, 2023

@sjohnr the thing is - while using CookieCsrfTokenRepository the raw CSRF token is used for doing all the to-be-secured HTTP communication. So my Web client only has the raw CSRF token available which was delivered by the cookie.

The default ChannelInterceptor which is used for websocket communication is XorCsrfChannelInterceptor though which expects the CSRF token to be in the "encrypted" format. Neither can I sent an "encrypted" CSRF token, nor is it stored in the CsrfTokenRepository in an "encrypted" way.

So I currently see no other way except supplying the CsrfChannelInterceptor bean to be used for handling CSRF websockets. when using the CookieCsrfTokenRepository on the HTTP part.

This is my SecurityConfiguration - The ::handle part is basically the same which is advertised here: 5.8 migration guide but less code ("_csrf" is the default)

This is my WebSocketConfiguration as well as my WebSocketSecurityConfiguration.

All the "hacks" where removed but thats the current setup I need to get it working with the current spring-security.

@sjohnr
Copy link
Contributor

sjohnr commented Feb 27, 2023

I see. Thanks @OlliL! There are a number of different permutations possible depending on both your client-side framework and app code, in addition to using websockets. I wasn't considering the fact that you're using an Angular-style integration that obtains the raw token from the cookie itself.

The CookieCsrfTokenRepository example could be modified to set a response header to obtain the hashed token.

Note that there is also an option listed in the migration guide demonstrating an example /csrf endpoint that would provide a way for you to obtain the hashed token, but I do not include an example of how to integrate with that endpoint. If you want to go with an out-of-the-box Spring Security setup, you will need to change your client-side app to obtain the hashed token somehow.

Having said that, it sounds like you found the correct configuration. I'm not sure how much more specific the migration guide can be (your case is probably a common one but still one of many). If you feel there's room for improvement in the docs, feel free to submit an issue with suggestions and I'll happily take a look!

@emopti-jrufer
Copy link
Contributor Author

@OlliL @sjohnr I am using Angular as our client-side framework. I ended up with a hybrid approach to use XorCsrfToken that combines using the session as via HttpSessionCsrfTokenRepository for storing the CsrfToken, a /csrf endpoint that is called prior to authenticating which populates the initial XSRF-TOKEN cookie utilized by Angular and finally created an AuthenticationSuccessHandler to repopulate the cookie post authentication since a new CsrfToken is created during authentication. Granted the part could be accomplished by calling the /csrf endpoint again.

The solution could be a lot simpler if it was natively in the framework. For example CsrfAuthenticationStrategy might be a better spot for setting the cookie post authentication.

I can share can the implementation or even contribute the framework if it would beneficial to others.

@sjohnr
Copy link
Contributor

sjohnr commented Feb 28, 2023

and finally created an AuthenticationSuccessHandler to repopulate the cookie post authentication since a new CsrfToken is created during authentication

I believe this is true because of HttpSessionCsrfTokenRepository. I don’t believe it is needed in the case of CookieCsrfTokenRepository. Can you explain why you found this necessary?

The solution could be a lot simpler if it was natively in the framework. For example CsrfAuthenticationStrategy might be a better spot for setting the cookie post authentication.

Can you elaborate on this point? What would be changed in the framework to make this work better for you? Keep in mind that the framework has to support both session and cookie-based CSRF tokens. Also bear in mind that the design consideration for deferred tokens is that the application itself should choose when the token is loaded. I don’t think the framework can make that choice for you.

@emopti-jrufer
Copy link
Contributor Author

@sjohnr
Overall I want to minimize the number of requests needed to get to an authenticated state in our application.

/csrf
/authenticate
/csrf (Eliminate the need for this request)
... remaining authenticated application requests

My first attempt at trying eliminate the need for the extra /csrf request using the CookieCsrfTokenRepository was to retrieve the token after the CsrfAuthenticationStrategy was invoked. The following are the options I was evaluating.

  1. I was not able to add an additional SessionAuthenticationStrategy after CsrfAuthenticationStrategy since org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer#addSessionAuthenticationStrategy is package private.
  2. Filter. There wasn't a great way of determining if a request had its token replaced without doing url matching on all authentication endpoints. Having a request attribute could make this cleaner.
  3. Custom AuthenticationSuccessHandler wrapper.

However all of these options have an issue where the csrf cookie was being set 2 times in the response. One for the clearing of the token in CsrfAuthenticationStrategy and one for the setting of the token which the browser was ignoring the second setter of the same cookie with the new token value.

Possible solutions:

  1. Add the capability to HttpSessionCsrfTokenRepository to also write a cookie along with adding an option into CsrfAuthenticationStrategy to eagerly persist the token. It may be necessary to relocate where the XorCsrfTokenRequestAttributeHandler.createXoredCsrfToken logic resides.
  2. In the CsrfAuthenticationStrategy instead of a clearing out the token (removing session attribute or setting cookie to null) just save the new token value. This could potentially be configurable via a flag that the application can control.

the application itself should choose when the token is loaded. I don’t think the framework can make that choice for you.

Could clarify the following for me? When referring to "loaded" are talking about retrieving the token or generating the new value?


The solution could be a lot simpler if it was natively in the framework.

This is referring to the locked down nature of the framework where certain internal apis are unavailable and classes are not extensible.


Note: The debug statement in CsrfAuthenticationStrategy#onAuthentication is a bit misleading since the token is actually not replaced at that point in time from a persistence (session / cookie) standpoint until something else tries to access the token.

@sjohnr
Copy link
Contributor

sjohnr commented Mar 2, 2023

@emopti-jrufer appreciate the extra details!

The behavior you're trying to puzzle out now is actually how to solve for deferred tokens in Spring Security 6, which the 5.8 migration guide covers in detail.

If you follow the guide and run into issues with deferred tokens, you have 3 options for handling deferred tokens:

If you had followed the upgrade path from 5.7 to 5.8 and performed these steps in order to see which one worked for you (before trying to enable BREACH protection in a later step), you should find that Option 3 solves your problem. This is because in your case, you're using .formLogin() with CookieCsrfTokenRepository. (Granted, when you were trying this, I hadn't yet updated the documentation to include these 3 options, so I'm describing the ideal path going forward that may not have been available for you at the time. 😉)

Next, when you try the steps in the next section to enable BREACH protection, you would find you have two options for handling it:

In your case, you have already found that Option 1 solves your problem. But because it wasn't combined with the solution from the deferred token section, you've struggled to get it working the way you'd like it to.

I recognize that this is a bit of a "choose your own adventure" book, but that's because there are so many possible ways you can choose to integrate the frontend with Spring Security. Your case is one among many.

Having said all that, here is what I believe should work for you:

@Configuration
@EnableWebSecurity
public class SecurityConfiguration {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        CookieCsrfTokenRepository csrfTokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse();
        XorCsrfTokenRequestAttributeHandler delegate = new XorCsrfTokenRequestAttributeHandler();
        delegate.setCsrfRequestAttributeName(null);

        // @formatter:off
        http
            .authorizeHttpRequests((authorize) -> authorize
                .anyRequest().authenticated()
            )
            .csrf((csrf) -> csrf
                .csrfTokenRequestHandler(delegate::handle)
                .csrfTokenRepository(csrfTokenRepository)
            )
            .formLogin(Customizer.withDefaults());
        // @formatter:on

        return http.build();
    }

}

This solution should not require any calls to the /csrf endpoint (in the case of the default login page), since it didn't solve your problem as described above. Though in your case, you may not have any other way of triggering the initial token to be loaded, so it's fine to keep it for the first call.


At this point, I'm not really sure how any of this relates to web socket support. I've included the explanation and example above as an exercise in discovering what (if anything) is missing from our documentation that would help users like yourself (who come into this a bit later than you did and can benefit from better docs). If you feel I'm missing something, please correct me where I'm wrong.

I feel like the main issue here is just following the migration guide top-to-bottom. If this is done, users will hopefully have a better experience. Can you think of anything I'm not considering?

@sjohnr
Copy link
Contributor

sjohnr commented Mar 3, 2023

I also missed answering your questions, sorry about that.

the application itself should choose when the token is loaded. I don’t think the framework can make that choice for you.

Could clarify the following for me? When referring to "loaded" are talking about retrieving the token or generating the new value?

Both. The application chooses to trigger loading the token via CsrfToken#getToken(). It is loaded or generated and saved if necessary via the DeferredCsrfToken interface.

The solution could be a lot simpler if it was natively in the framework.

This is referring to the locked down nature of the framework where certain internal apis are unavailable and classes are not extensible.

All of the components you mentioned can be extended through delegation. You can use CsrfConfigurer#sessionAuthenticationStrategy instead of SessionManagementConfigurer#addSessionAuthenticationStrategy. It is possible to achieve your desired results this way, but the configuration I provided above is simpler.

@emopti-jrufer
Copy link
Contributor Author

@sjohnr Is there a reason this information would be documented in a migration guide? Isn't this information still relevant in the latest version? We started on Spring 6 so following a migration guide for prior versions seems counterintuitive. That said I will try out your suggestions and if needed open separate issues or post on Stack Overflow.

@sjohnr
Copy link
Contributor

sjohnr commented Mar 7, 2023

Is there a reason this information would be documented in a migration guide? Isn't this information still relevant in the latest version?

Thanks for the question, @emopti-jrufer! So as not to encourage a forum-like discussion here, I'll keep my answer brief.

Since there were numerous highly impactful CSRF-related changes in Spring Security 6, I wanted to address upgrade issues first. Any positive feedback (this is usually rare 😉) would be used to make improvements on the main documentation for CSRF as time allows. Documentation improvements are a theme we're focusing on, so now is a good time to address it.

We're also always looking for individuals who would be interested in making improvements to our documentation, as it tends to be an easy way to get involved with open source without a huge time commitment. If you're interested in helping, let us know by chiming in on an issue! I'll open a new issue for CSRF documentation enhancements when I get some time this week.

@HJK181
Copy link

HJK181 commented Apr 13, 2023

@sjohnr Sry for using this as a forum-like discussion, but I need to know if the configuration is the right approach for an reactive SecurityWebFilterChain as it's not mentioned in the documentation:

@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {
	XorServerCsrfTokenRequestAttributeHandler delegate = new XorServerCsrfTokenRequestAttributeHandler();
	http.csrf(csrf -> csrf
		.csrfTokenRepository(cookieCsrfTokenRepository())
		.csrfTokenRequestHandler(delegate::handle))
	.formLogin(Customizer.withDefaults());

I'm asking as there's no way to call setCsrfRequestAttributeName(null) on the reactive impl.

@sjohnr
Copy link
Contributor

sjohnr commented Apr 13, 2023

@HJK181, is this what you're looking for? https://docs.spring.io/spring-security/reference/5.8/migration/reactive.html#_i_am_using_angularjs_or_another_javascript_framework

@HJK181
Copy link

HJK181 commented Apr 13, 2023

Shame on me, did not see the reactive menu entry on the left-hand side, thx a lot.

@ptahchiev
Copy link

Does this mean the websockets are broken in the 5.x branch? Any chance to have this ported to 5.x?

@sjohnr
Copy link
Contributor

sjohnr commented Aug 15, 2023

@ptahchiev not that I'm aware of. If you find anything, please open an issue. Please note that this support was added to 5.8 (see gh-12562).

@ismail-bertalfilali
Copy link

ismail-bertalfilali commented May 29, 2024

To disable CSRF with stomp spring boot 3 just add this code in your configuration class, the bean name must be csrfChannelInterceptor :

@Bean
    public ChannelInterceptor csrfChannelInterceptor(){
        //disabling csrf
        return new ChannelInterceptor() {

        };
    }
assuming that you are using @EnableWebSocketSecurity 

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: messaging An issue in spring-security-messaging type: bug A general bug
Projects
None yet
Development

No branches or pull requests

9 participants