WebSockets power everything from chat apps to financial tickers. But their persistent connections create unique attack surfaces that traditional HTTP security doesn't address.
1. Authenticate Before Upgrading
Never establish WebSocket connections before verifying identity:
const WebSocket = require('ws');
const jwt = require('jsonwebtoken');
const wss = new WebSocket.Server({ noServer: true });
server.on('upgrade', (request, socket, head) => {
const token = new URL(request.url, 'ws://localhost').searchParams.get('token');
try {
const user = jwt.verify(token, process.env.JWT_SECRET);
wss.handleUpgrade(request, socket, head, (ws) => {
ws.user = user;
wss.emit('connection', ws, request);
});
} catch (err) {
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
socket.destroy();
}
});
2. Rate Limit Messages per Connection
Prevent abuse through message-level rate limiting:
class RateLimitedWebSocket {
constructor(ws, maxMessages = 50, windowMs = 60000) {
this.ws = ws;
this.messages = [];
this.maxMessages = maxMessages;
this.windowMs = windowMs;
ws.on('message', (data) => this.handleMessage(data));
}
handleMessage(data) {
const now = Date.now();
this.messages = this.messages.filter(t => now - t < this.windowMs);
if (this.messages.length >= this.maxMessages) {
this.ws.send(JSON.stringify({
error: 'Rate limit exceeded',
retryAfter: this.windowMs / 1000
}));
return;
}
this.messages.push(now);
this.processMessage(data);
}
processMessage(data) {
// Your message handling logic
}
}
wss.on('connection', (ws) => {
new RateLimitedWebSocket(ws);
});
3. Validate and Sanitize All Messages
Treat WebSocket data like any other user input:
const Joi = require('joi');
const messageSchema = Joi.object({
type: Joi.string().valid('chat', 'typing', 'reaction').required(),
content: Joi.string().max(1000).required(),
roomId: Joi.string().uuid().required()
});
ws.on('message', async (rawData) => {
try {
const message = JSON.parse(rawData);
const { error, value } = messageSchema.validate(message);
if (error) {
ws.send(JSON.stringify({ error: 'Invalid message format' }));
return;
}
// Verify user has access to room
const hasAccess = await checkRoomAccess(ws.user.id, value.roomId);
if (!hasAccess) {
ws.send(JSON.stringify({ error: 'Access denied' }));
return;
}
await broadcastToRoom(value.roomId, value);
} catch (err) {
ws.send(JSON.stringify({ error: 'Message processing failed' }));
}
});
4. Implement Connection Timeouts
Prevent resource exhaustion from abandoned connections:
function setupHeartbeat(ws, interval = 30000, timeout = 5000) {
let isAlive = true;
ws.on('pong', () => { isAlive = true; });
const heartbeat = setInterval(() => {
if (!isAlive) {
clearInterval(heartbeat);
ws.terminate();
return;
}
isAlive = false;
ws.ping();
}, interval);
ws.on('close', () => clearInterval(heartbeat));
}
wss.on('connection', (ws) => {
setupHeartbeat(ws);
});
5. Secure Message Broadcasting
Ensure messages only reach authorized recipients:
class SecureRoom {
constructor(roomId) {
this.roomId = roomId;
this.connections = new Map();
}
async addConnection(ws, userId) {
const permissions = await getUserRoomPermissions(userId, this.roomId);
this.connections.set(ws, { userId, permissions });
ws.on('close', () => this.connections.delete(ws));
}
broadcast(message, requiredPermission = 'read') {
const payload = JSON.stringify(message);
for (const [ws, { permissions }] of this.connections) {
if (permissions.includes(requiredPermission)) {
ws.send(payload);
}
}
}
}
WebSockets operate outside the request-response paradigm. Traditional middleware doesn't apply, you need specialized security patterns for persistent connections.
Thanks for reading! How do you secure real-time features in your applications?
Building real-time systems that don't compromise on security? Let's collaborate: ILLAPEX
Top comments (2)
Interestingly, XMPP over Websocket doesn't obey your first rule - because it embeds the authentication into the Websocket channel itself.
This can be more powerful - in the case of XMPP, there's typically no authentication to anything except the Websocket, so HTTP compatibility isn't needed, and we can rely on SASL. SASL allows for complex challenge/response flows which are more protective of the password, and more resistent to replay attacks etc.
Great breakdown! πβ‘
Real-time apps often feel like theyβre running on βtrust falls,β but your security steps turn WebSockets into a fortress instead of a free-for-all.
Really love the authenticate-before-upgrade approach!! because if someoneβs trying to sneak into the socket without a valid token, thatβs basically a digital version of βwho are you and how did you get in my house?β π
The rate limiting, schema validation, and heartbeat checks make the whole setup feel production-ready and disaster-proof.