WebSocket Security: 6 Vulnerabilities Developers Always Miss

SecureCodeReviews Team
January 20, 2025
13 min read
316 words
Share

Why WebSocket Security Gets Overlooked

WebSockets provide real-time, bidirectional communication — powering chat apps, live dashboards, collaborative editors, and trading platforms. But they bypass most traditional HTTP security controls:

  • ❌ No CORS protection (WebSockets don't follow same-origin policy the same way)
  • ❌ No built-in authentication per message
  • ❌ No automatic CSRF protection
  • ❌ WAFs often can't inspect WebSocket traffic
  • ❌ Rate limiting is harder to implement

In our security audits, 90% of WebSocket implementations had at least one critical vulnerability.


Vulnerability #1: Cross-Site WebSocket Hijacking (CSWSH)

This is the WebSocket equivalent of CSRF — and it's critical.

How It Works

  1. User is logged into app.example.com (has session cookie)
  2. User visits evil.com
  3. evil.com opens a WebSocket to app.example.com/ws
  4. Browser sends the session cookie automatically
  5. Attacker's page now has an authenticated WebSocket connection

❌ Vulnerable Server

const WebSocket = require('ws');
const wss = new WebSocket.Server({ server });

wss.on('connection', (ws, req) => {
  // No origin check!
  // Cookie is sent automatically — attacker has authenticated connection
  const session = getSessionFromCookie(req.headers.cookie);
  ws.userId = session.userId;
});

✅ Fixed Server

wss.on('connection', (ws, req) => {
  // Check Origin header
  const origin = req.headers.origin;
  const allowedOrigins = ['https://app.example.com'];

  if (!allowedOrigins.includes(origin)) {
    ws.close(1008, 'Origin not allowed');
    return;
  }

  // Better: Use ticket-based auth instead of cookies
  const ticket = new URL(req.url, 'http://localhost').searchParams.get('ticket');
  const session = validateOneTimeTicket(ticket);

  if (!session) {
    ws.close(1008, 'Invalid ticket');
    return;
  }

  ws.userId = session.userId;
});

Vulnerability #2: No Authentication After Handshake

The initial WebSocket handshake might be authenticated, but subsequent messages aren't verified.

❌ Vulnerable Pattern

wss.on('connection', (ws, req) => {
  const user = authenticateRequest(req); // Auth only at connection time

  ws.on('message', (data) => {
    const msg = JSON.parse(data);
    // No re-verification — what if session expired?
    // What if user's permissions changed?
    handleMessage(ws, msg);
  });
});

✅ Fix: Per-Message Token Validation

wss.on('connection', (ws, req) => {
  let currentUser = authenticateRequest(req);

  ws.on('message', async (data) => {
    const msg = JSON.parse(data);

    // Periodically revalidate (every 5 minutes)
    if (Date.now() - ws.lastAuthCheck > 300000) {
      currentUser = await revalidateSession(ws.sessionId);
      if (!currentUser) {
        ws.close(1008, 'Session expired');
        return;
      }
      ws.lastAuthCheck = Date.now();
    }

    // Check permissions for this specific action
    if (!currentUser.can(msg.action)) {
      ws.send(JSON.stringify({ error: 'Forbidden' }));
      return;
    }

    handleMessage(ws, currentUser, msg);
  });
});

Vulnerability #3: No Input Validation on Messages

❌ Dangerous: Trusting Client Messages

ws.on('message', (data) => {
  const msg = JSON.parse(data);

  switch (msg.action) {
    case 'updateProfile':
      // Directly using client-supplied userId — IDOR!
      db.users.update(msg.userId, { name: msg.name });
      break;
    case 'sendMessage':
      // No sanitization — stored XSS when displayed!
      db.messages.insert({
        text: msg.text,
        room: msg.room,
      });
      break;
  }
});

✅ Fix: Validate and Sanitize Everything

import { z } from 'zod';
import DOMPurify from 'isomorphic-dompurify';

const messageSchemas = {
  updateProfile: z.object({
    action: z.literal('updateProfile'),
    name: z.string().min(1).max(100).regex(/^[a-zA-Z\s]+$/),
  }),
  sendMessage: z.object({
    action: z.literal('sendMessage'),
    text: z.string().min(1).max(5000),
    room: z.string().uuid(),
  }),
};

ws.on('message', (data) => {
  let msg;
  try {
    msg = JSON.parse(data);
  } catch {
    ws.send(JSON.stringify({ error: 'Invalid JSON' }));
    return;
  }

  const schema = messageSchemas[msg.action];
  if (!schema) {
    ws.send(JSON.stringify({ error: 'Unknown action' }));
    return;
  }

  const result = schema.safeParse(msg);
  if (!result.success) {
    ws.send(JSON.stringify({ error: result.error.message }));
    return;
  }

  // Use server-side userId, sanitize text
  if (msg.action === 'sendMessage') {
    msg.text = DOMPurify.sanitize(msg.text);
  }

  handleMessage(ws, ws.userId, result.data); // Use ws.userId, not msg.userId
});

Vulnerability #4: No Rate Limiting

WebSockets maintain a persistent connection, so an attacker can flood messages without opening new connections.

✅ Fix: Message Rate Limiting

const rateLimiter = new Map();

ws.on('message', (data) => {
  const now = Date.now();
  const userLimits = rateLimiter.get(ws.userId) || { count: 0, windowStart: now };

  if (now - userLimits.windowStart > 60000) {
    // Reset window
    userLimits.count = 0;
    userLimits.windowStart = now;
  }

  userLimits.count++;
  rateLimiter.set(ws.userId, userLimits);

  if (userLimits.count > 100) {  // Max 100 messages per minute
    ws.send(JSON.stringify({ error: 'Rate limit exceeded' }));
    return;
  }

  handleMessage(ws, data);
});

Vulnerability #5: Leaking Data via Broadcast

❌ Broadcasting Sensitive Data to All Clients

// When an order is updated, broadcast to all connected clients
function notifyOrderUpdate(order) {
  wss.clients.forEach((client) => {
    // Every connected user sees every order!
    client.send(JSON.stringify({
      type: 'orderUpdate',
      order: order,  // Includes other users' data
    }));
  });
}

✅ Fix: Room-Based + Permission-Filtered Broadcasting

function notifyOrderUpdate(order) {
  wss.clients.forEach((client) => {
    // Only send to the order owner or admin
    if (client.userId === order.userId || client.role === 'admin') {
      // Filter sensitive fields based on role
      const filtered = client.role === 'admin'
        ? order
        : { id: order.id, status: order.status, total: order.total };

      client.send(JSON.stringify({
        type: 'orderUpdate',
        order: filtered,
      }));
    }
  });
}

Vulnerability #6: No TLS (wss://)

// ❌ Unencrypted — anyone on the network can read/modify messages
const ws = new WebSocket('ws://api.example.com/ws');

// ✅ Always use TLS
const ws = new WebSocket('wss://api.example.com/ws');

WebSocket Security Checklist

ControlPriority
Origin validation (CSWSH prevention)Critical
Ticket-based auth (not cookie-based)Critical
Per-message input validation (Zod/Joi)Critical
Periodic session revalidationHigh
Message rate limitingHigh
Room-based authorization for broadcastsHigh
TLS (wss://) everywhereCritical
Message size limitsMedium
Connection timeout/heartbeatMedium

Need a WebSocket Security Review?

We audit WebSocket implementations, Socket.IO servers, and real-time APIs. Request a free sample review →


Published by the SecureCodeReviews.com team — securing real-time applications for production.

Editorial standards

Published by SecureCodeReviews

This article is part of our original AI security and cybersecurity content library. We show publish and update dates, keep company and policy pages public, and update important guidance when material changes affect readers.

Named author: SecureCodeReviews Team
Published: Jan 20, 2025
Update status: current publication version

Questions or corrections?

Review our editorial standards, learn more about the company, or contact us if a page needs clarification.

Secure Code Review

Want an expert review before this issue reaches production?

We combine manual code review with AppSec tooling to find vulnerabilities, logic flaws, and insecure patterns before release or audit deadlines.

Manual secure code review for real exploitable issues
Remediation guidance with clear engineering next steps
Useful for launch reviews, client audits, and security hardening

Talk to SecureCodeReviews

Get a scoped review path fast

Manual review
Actionable fixes
Fast turnaround
Security-focused

Advertisement