or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

authorization.mdcsrf-protection.mdexpression-handling.mdindex.mdmessage-matching.mdreactive-support.mdsecurity-context.md
tile.json

csrf-protection.mddocs/

CSRF Protection

Protect WebSocket connections from Cross-Site Request Forgery attacks using token validation during handshake and message processing.

Overview

CSRF protection for WebSocket connections requires two components:

  1. CsrfTokenHandshakeInterceptor: Loads CSRF token during WebSocket handshake
  2. CsrfChannelInterceptor or XorCsrfChannelInterceptor: Validates token in CONNECT messages

Important: CSRF validation only occurs for CONNECT messages. Subsequent messages (MESSAGE, SUBSCRIBE) do not require CSRF tokens.

Capabilities

CsrfTokenHandshakeInterceptor

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;
        }
    }
}

CsrfChannelInterceptor

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

XorCsrfChannelInterceptor

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());
    }
}

Configuration Patterns

Basic CSRF Protection

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;
    }
}

Enhanced CSRF Protection with XOR Masking

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;
    }
}

Client-Side JavaScript Integration

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 };
}

Disabling CSRF Protection (Not Recommended)

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.

Choosing CSRF Interceptor

CsrfChannelInterceptor (Standard)

  • Use when: Standard CSRF protection is sufficient
  • Behavior: Validates unmasked CSRF tokens
  • Best for: Most applications, simpler configuration
  • Performance: Slightly faster (no unmasking required)

XorCsrfChannelInterceptor (Enhanced)

  • Use when: Enhanced security is required
  • Behavior: Validates XOR-masked CSRF tokens for added protection against token leakage
  • Best for: High-security applications, compliance requirements
  • Requires: XorCsrfTokenRequestAttributeHandler configured in HTTP security
  • Performance: Slightly slower (unmasking required)

Note: Choose ONE interceptor type - do not mix CsrfChannelInterceptor and XorCsrfChannelInterceptor. Mixing will cause validation failures.

Edge Cases and Error Scenarios

Token Expiration

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();
        });
    }
};

Missing Token in Handshake

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

Multiple WebSocket Endpoints

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();
}

Troubleshooting

Issue: Missing CSRF Token

Symptom: CONNECT messages fail with "Invalid CSRF token" or "Missing CSRF token"

Causes:

  1. CsrfTokenHandshakeInterceptor not registered
  2. CSRF token not available in HTTP request
  3. Token not included in CONNECT frame headers

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();
}

Issue: Client Not Sending CSRF Token

Symptom: Server has CSRF protection but client connections fail

Causes:

  1. Token not extracted from page
  2. Token not included in CONNECT headers
  3. Wrong header name used

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, ...);

Issue: Interceptor Mismatch

Symptom: CSRF validation fails despite correct token

Causes:

  1. Channel interceptor doesn't match token handler type
  2. Token format mismatch (masked vs unmasked)

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: XorCsrfChannelInterceptor

Issue: Token Validation Only on CONNECT

Symptom: 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.

Performance Considerations

  • Handshake overhead: ~0.5ms for token loading
  • CONNECT validation: ~0.2ms per CONNECT message
  • Subsequent messages: No CSRF overhead (validation skipped)
  • Token size: Typically 32-64 bytes

For high-throughput scenarios:

  • CSRF validation only occurs once per connection (on CONNECT)
  • Subsequent messages have no CSRF overhead
  • Consider caching token validation results if needed