Protect WebSocket connections from Cross-Site Request Forgery attacks using token validation during handshake and message processing.
CSRF protection for WebSocket connections requires two components:
Important: CSRF validation only occurs for CONNECT messages. Subsequent messages (MESSAGE, SUBSCRIBE) do not require CSRF tokens.
Loads CSRF token from HTTP request during WebSocket handshake and stores it in WebSocket session attributes for later validation by channel interceptors. Thread-safe.
/**
* HandshakeInterceptor that loads CsrfToken from HTTP request during
* WebSocket handshake and stores in WebSocket session attributes.
* Thread-safe.
*
* @since 4.0
*/
public final class CsrfTokenHandshakeInterceptor implements HandshakeInterceptor {
/**
* Loads CSRF token from request and stores in WebSocket attributes.
* Token is extracted from request attributes (set by CsrfFilter).
*
* @param request the HTTP request (must not be null)
* @param response the HTTP response (must not be null)
* @param wsHandler the WebSocket handler (must not be null)
* @param attributes WebSocket session attributes (mutable, must not be null)
* @return true to proceed with handshake, false to reject
* @throws IllegalArgumentException if any parameter is null
*/
public boolean beforeHandshake(
ServerHttpRequest request,
ServerHttpResponse response,
WebSocketHandler wsHandler,
Map<String, Object> attributes
);
/**
* Called after handshake completion (no-op implementation).
*
* @param request the HTTP request (must not be null)
* @param response the HTTP response (must not be null)
* @param wsHandler the WebSocket handler (must not be null)
* @param exception exception if handshake failed (can be null)
*/
public void afterHandshake(
ServerHttpRequest request,
ServerHttpResponse response,
WebSocketHandler wsHandler,
Exception exception
);
}Usage Example:
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.security.messaging.web.socket.server.CsrfTokenHandshakeInterceptor;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.addInterceptors(new CsrfTokenHandshakeInterceptor())
.withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic", "/queue");
config.setApplicationDestinationPrefixes("/app");
}
}Error Handling:
// Custom handshake interceptor with error handling
public class ErrorHandlingCsrfTokenHandshakeInterceptor
extends CsrfTokenHandshakeInterceptor {
@Override
public boolean beforeHandshake(
ServerHttpRequest request,
ServerHttpResponse response,
WebSocketHandler wsHandler,
Map<String, Object> attributes
) {
try {
return super.beforeHandshake(request, response, wsHandler, attributes);
} catch (Exception e) {
logger.error("Failed to load CSRF token during handshake", e);
// Reject handshake if CSRF token cannot be loaded
return false;
}
}
}Validates CSRF token in CONNECT messages to prevent CSRF attacks on WebSocket connections. Uses standard CSRF token validation. Thread-safe.
/**
* ChannelInterceptor that validates CSRF token in CONNECT messages.
* Expects token to be present in message headers (loaded by handshake interceptor).
* Only validates CONNECT messages; other message types are passed through.
* Thread-safe.
*
* @since 4.0
*/
public final class CsrfChannelInterceptor implements ChannelInterceptor {
/**
* Intercepts message before sending and validates CSRF token for CONNECT messages.
* Non-CONNECT messages are passed through without validation.
*
* @param message the message (must include CSRF token in headers for CONNECT)
* @param channel the channel (must not be null)
* @return the message if valid (never null)
* @throws InvalidCsrfTokenException if CSRF token is invalid or missing for CONNECT messages
* @throws IllegalArgumentException if message or channel is null
*/
public Message<?> preSend(Message<?> message, MessageChannel channel);
}Usage Example:
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.security.config.annotation.web.socket.AbstractSecurityWebSocketMessageBrokerConfigurer;
import org.springframework.security.messaging.web.csrf.CsrfChannelInterceptor;
import org.springframework.security.web.csrf.InvalidCsrfTokenException;
@Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
@Override
protected void configureInbound(ChannelRegistration registration) {
registration.interceptors(new CsrfChannelInterceptor());
}
}Error Handling:
// Global exception handler for CSRF errors
@ControllerAdvice
public class WebSocketExceptionHandler {
@MessageExceptionHandler(InvalidCsrfTokenException.class)
public void handleInvalidCsrfToken(InvalidCsrfTokenException ex) {
logger.warn("CSRF token validation failed: {}", ex.getMessage());
// Send error to client
}
}Validates XOR-masked CSRF tokens in CONNECT messages. Used with XorCsrfTokenRequestAttributeHandler for enhanced security through token masking. Thread-safe.
/**
* ChannelInterceptor that validates XOR-masked CSRF tokens in CONNECT messages.
* Use with XorCsrfTokenRequestAttributeHandler for enhanced CSRF protection.
* Only validates CONNECT messages; other message types are passed through.
* Thread-safe.
*
* @since 5.8
*/
public final class XorCsrfChannelInterceptor implements ChannelInterceptor {
/**
* Intercepts message and validates XOR-masked CSRF token for CONNECT messages.
* Non-CONNECT messages are passed through without validation.
*
* @param message the message (must include XOR-masked CSRF token for CONNECT)
* @param channel the channel (must not be null)
* @return the message if valid (never null)
* @throws InvalidCsrfTokenException if CSRF token is invalid or missing for CONNECT messages
* @throws IllegalArgumentException if message or channel is null
*/
public Message<?> preSend(Message<?> message, MessageChannel channel);
}Usage Example:
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.security.config.annotation.web.socket.AbstractSecurityWebSocketMessageBrokerConfigurer;
import org.springframework.security.messaging.web.csrf.XorCsrfChannelInterceptor;
@Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
@Override
protected void configureInbound(ChannelRegistration registration) {
// Use XOR CSRF interceptor for enhanced security
registration.interceptors(new XorCsrfChannelInterceptor());
}
}import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.security.config.annotation.web.socket.AbstractSecurityWebSocketMessageBrokerConfigurer;
import org.springframework.security.messaging.web.csrf.CsrfChannelInterceptor;
import org.springframework.security.messaging.web.socket.server.CsrfTokenHandshakeInterceptor;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
@Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// Add CSRF token to WebSocket session during handshake
registry.addEndpoint("/ws")
.addInterceptors(new CsrfTokenHandshakeInterceptor())
.withSockJS();
}
@Override
protected void configureInbound(ChannelRegistration registration) {
// Validate CSRF token on CONNECT messages
registration.interceptors(new CsrfChannelInterceptor());
}
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic", "/queue");
config.setApplicationDestinationPrefixes("/app");
}
@Override
protected boolean sameOriginDisabled() {
// Disable same-origin enforcement when using CSRF tokens
return true;
}
}import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.socket.AbstractSecurityWebSocketMessageBrokerConfigurer;
import org.springframework.security.messaging.web.csrf.XorCsrfChannelInterceptor;
import org.springframework.security.messaging.web.socket.server.CsrfTokenHandshakeInterceptor;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.csrf.XorCsrfTokenRequestAttributeHandler;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
@Configuration
public class EnhancedWebSocketSecurityConfig
extends AbstractSecurityWebSocketMessageBrokerConfigurer {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// Configure HTTP security to use XOR CSRF token handling
XorCsrfTokenRequestAttributeHandler requestHandler =
new XorCsrfTokenRequestAttributeHandler();
requestHandler.setCsrfRequestAttributeName("_csrf");
http
.csrf(csrf -> csrf
.csrfTokenRequestHandler(requestHandler)
);
return http.build();
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.addInterceptors(new CsrfTokenHandshakeInterceptor())
.withSockJS();
}
@Override
protected void configureInbound(ChannelRegistration registration) {
// Use XOR interceptor to match XOR token handler
registration.interceptors(new XorCsrfChannelInterceptor());
}
@Override
protected boolean sameOriginDisabled() {
return true;
}
}When using CSRF protection, clients must include the CSRF token in their WebSocket CONNECT frames. The token must be obtained from the HTTP session (via cookie, header, or meta tag) and included in the CONNECT frame headers.
HTML Template (Thymeleaf example):
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta name="_csrf" th:content="${_csrf.token}"/>
<meta name="_csrf_header" th:content="${_csrf.headerName}"/>
<script src="/webjars/sockjs-client/sockjs.min.js"></script>
<script src="/webjars/stomp-websocket/stomp.min.js"></script>
</head>
<body>
<script th:inline="javascript">
// Extract CSRF token from meta tag
var token = document.querySelector('meta[name="_csrf"]').getAttribute('content');
var header = document.querySelector('meta[name="_csrf_header"]').getAttribute('content');
// Connect to WebSocket with CSRF token
var socket = new SockJS('/ws');
var stompClient = Stomp.over(socket);
// Include CSRF token in CONNECT frame headers
var headers = {};
if (token && header) {
headers[header] = token;
}
stompClient.connect(headers,
function onConnect(frame) {
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/messages', function(message) {
console.log('Received: ' + message.body);
});
},
function onError(error) {
console.error('Connection error:', error);
// Handle CSRF token errors
if (error.headers && error.headers['message'] &&
error.headers['message'].includes('CSRF')) {
console.error('CSRF token validation failed');
// Refresh page to get new token
location.reload();
}
}
);
// Send message
function sendMessage(message) {
stompClient.send("/app/chat", {}, JSON.stringify({
'content': message
}));
}
</script>
</body>
</html>Plain JavaScript Example with Error Handling:
// Fetch CSRF token from cookie or meta tag
function getCsrfToken() {
// Try meta tag first
const tokenMeta = document.querySelector('meta[name="_csrf"]');
if (tokenMeta) {
return tokenMeta.getAttribute('content');
}
// Try cookie
const cookies = document.cookie.split(';');
for (let cookie of cookies) {
const [name, value] = cookie.trim().split('=');
if (name === 'XSRF-TOKEN') {
return decodeURIComponent(value);
}
}
return null;
}
function getCsrfHeader() {
const headerMeta = document.querySelector('meta[name="_csrf_header"]');
return headerMeta ? headerMeta.getAttribute('content') : 'X-CSRF-TOKEN';
}
// Create WebSocket connection with CSRF protection
function connectWebSocket() {
const socket = new SockJS('/ws');
const stompClient = Stomp.over(socket);
const csrfToken = getCsrfToken();
const csrfHeader = getCsrfHeader();
if (!csrfToken) {
console.error('CSRF token not found');
return null;
}
const connectHeaders = {};
connectHeaders[csrfHeader] = csrfToken;
stompClient.connect(connectHeaders,
function onConnect(frame) {
console.log('WebSocket connected', frame);
// Subscribe to destinations
stompClient.subscribe('/user/queue/messages', function(message) {
handleMessage(JSON.parse(message.body));
});
},
function onError(error) {
console.error('WebSocket connection error', error);
// Check for CSRF token errors
if (error.headers && error.headers['message']) {
const message = error.headers['message'];
if (message.includes('CSRF') || message.includes('Invalid token')) {
console.error('CSRF token validation failed. Refreshing...');
// Refresh to get new token
setTimeout(() => location.reload(), 1000);
return;
}
}
// Handle other errors
handleConnectionError(error);
}
);
return stompClient;
}
function handleConnectionError(error) {
// Implement error handling logic
console.error('Connection error details:', error);
}React Example:
import { useEffect, useState } from 'react';
import SockJS from 'sockjs-client';
import { Client } from '@stomp/stompjs';
function useWebSocket(url) {
const [stompClient, setStompClient] = useState(null);
const [connected, setConnected] = useState(false);
useEffect(() => {
// Get CSRF token from meta tag or cookie
const getCsrfToken = () => {
const meta = document.querySelector('meta[name="_csrf"]');
return meta ? meta.getAttribute('content') : null;
};
const getCsrfHeader = () => {
const meta = document.querySelector('meta[name="_csrf_header"]');
return meta ? meta.getAttribute('content') : 'X-CSRF-TOKEN';
};
const token = getCsrfToken();
const header = getCsrfHeader();
if (!token) {
console.error('CSRF token not found');
return;
}
const client = new Client({
webSocketFactory: () => new SockJS(url),
connectHeaders: {
[header]: token
},
onConnect: () => {
setConnected(true);
},
onStompError: (frame) => {
console.error('STOMP error:', frame);
if (frame.headers && frame.headers['message']?.includes('CSRF')) {
console.error('CSRF validation failed');
window.location.reload();
}
},
onWebSocketError: (error) => {
console.error('WebSocket error:', error);
}
});
client.activate();
setStompClient(client);
return () => {
client.deactivate();
};
}, [url]);
return { stompClient, connected };
}For development or when CSRF protection is handled differently:
@Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// No CsrfTokenHandshakeInterceptor
registry.addEndpoint("/ws").withSockJS();
}
@Override
protected void configureInbound(ChannelRegistration registration) {
// No CsrfChannelInterceptor
// Add other interceptors as needed
}
@Override
protected boolean sameOriginDisabled() {
// Keep same-origin check enabled when not using CSRF tokens
return false;
}
}Warning: Only disable CSRF protection in development environments or when you have alternative CSRF mitigation strategies in place. In production, always use CSRF protection.
Note: Choose ONE interceptor type - do not mix CsrfChannelInterceptor and XorCsrfChannelInterceptor. Mixing will cause validation failures.
CSRF tokens may expire. Handle token refresh:
// Check for token expiration and refresh
function refreshCsrfToken() {
return fetch('/api/csrf-token', {
method: 'GET',
credentials: 'include'
})
.then(response => response.json())
.then(data => {
// Update meta tag or cookie
const meta = document.querySelector('meta[name="_csrf"]');
if (meta) {
meta.setAttribute('content', data.token);
}
return data.token;
});
}
// Reconnect with new token on CSRF error
stompClient.onStompError = function(frame) {
if (frame.headers['message']?.includes('CSRF')) {
refreshCsrfToken().then(token => {
// Reconnect with new token
connectWebSocket();
});
}
};If CSRF token is not available during handshake:
// Custom interceptor that handles missing tokens
public class OptionalCsrfTokenHandshakeInterceptor
extends CsrfTokenHandshakeInterceptor {
private final boolean requireToken;
public OptionalCsrfTokenHandshakeInterceptor(boolean requireToken) {
this.requireToken = requireToken;
}
@Override
public boolean beforeHandshake(...) {
try {
return super.beforeHandshake(...);
} catch (Exception e) {
if (requireToken) {
logger.error("CSRF token required but not found", e);
return false; // Reject handshake
} else {
logger.warn("CSRF token not found, proceeding without it", e);
return true; // Allow handshake
}
}
}
}When using multiple endpoints, ensure CSRF protection is configured for each:
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// Public endpoint (no CSRF)
registry.addEndpoint("/ws/public")
.withSockJS();
// Protected endpoint (with CSRF)
registry.addEndpoint("/ws/secure")
.addInterceptors(new CsrfTokenHandshakeInterceptor())
.withSockJS();
}Symptom: CONNECT messages fail with "Invalid CSRF token" or "Missing CSRF token"
Causes:
Solutions:
// Ensure interceptor is registered
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.addInterceptors(new CsrfTokenHandshakeInterceptor()) // Must be added
.withSockJS();
}
// Ensure HTTP security has CSRF enabled
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
return http
.csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))
.build();
}Symptom: Server has CSRF protection but client connections fail
Causes:
Solutions:
// Ensure token is extracted correctly
const token = document.querySelector('meta[name="_csrf"]')?.getAttribute('content');
const header = document.querySelector('meta[name="_csrf_header"]')?.getAttribute('content');
// Ensure token is included in CONNECT
const headers = {};
if (token && header) {
headers[header] = token;
}
stompClient.connect(headers, ...);Symptom: CSRF validation fails despite correct token
Causes:
Solutions:
// Match interceptor with handler
// Standard:
http.csrf(csrf -> csrf.csrfTokenRepository(...));
// Use: CsrfChannelInterceptor
// XOR:
XorCsrfTokenRequestAttributeHandler handler = new XorCsrfTokenRequestAttributeHandler();
http.csrf(csrf -> csrf.csrfTokenRequestHandler(handler));
// Use: XorCsrfChannelInterceptorSymptom: Expecting validation on all messages but only CONNECT is validated
Explanation: This is by design. CSRF protection only validates CONNECT messages. Subsequent messages (MESSAGE, SUBSCRIBE) rely on the established WebSocket connection for security.
Solution: This is correct behavior. If you need message-level validation, use authorization rules instead.
For high-throughput scenarios: