WebSocket Security: 6 Vulnerabilities Developers Always Miss
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.
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.