WebSocket Security: 6 Vulnerabilities Developers Always Miss
On this page
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
- User is logged into
app.example.com(has session cookie) - User visits
evil.com evil.comopens a WebSocket toapp.example.com/ws- Browser sends the session cookie automatically
- 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
| Control | Priority |
|---|---|
| Origin validation (CSWSH prevention) | Critical |
| Ticket-based auth (not cookie-based) | Critical |
| Per-message input validation (Zod/Joi) | Critical |
| Periodic session revalidation | High |
| Message rate limiting | High |
| Room-based authorization for broadcasts | High |
| TLS (wss://) everywhere | Critical |
| Message size limits | Medium |
| Connection timeout/heartbeat | Medium |
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.
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.
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.
Advertisement
Free Security Tools
Try our tools now
Expert Services
Get professional help
OWASP Top 10
Learn the top risks
Related Articles
OWASP Top 10 2025: What's Changed and How to Prepare
A comprehensive breakdown of the latest OWASP Top 10 vulnerabilities and actionable steps to secure your applications against them.
SQL Injection Prevention: Complete Guide with Code Examples
Master SQL injection attacks and learn proven prevention techniques. Includes vulnerable code examples, parameterized queries, and real-world breach analysis.
XSS (Cross-Site Scripting) Prevention: Complete Guide 2025
Learn to prevent Stored, Reflected, and DOM-based XSS attacks. Includes real examples, OWASP prevention strategies, and Content Security Policy implementation.