Skip to content

Fix CSRF protection provided by @EnableWebSocketSecurity / Stomp #12378

@emopti-jrufer

Description

@emopti-jrufer

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

Metadata

Metadata

Assignees

Labels

in: messagingAn issue in spring-security-messagingtype: bugA general bug

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions