WebSocket Security Checklist: 10 Controls Before You Ship to Production
A production WebSocket security checklist covering origin validation, authentication, rate limiting, TLS, payload sanitization, and DoS protection before go-live.
WebSocket endpoints are persistently open channels. Once a connection is established, it bypasses the standard HTTP request/response cycle — and with it, many of the security controls developers have learned to apply automatically. An insecure WebSocket endpoint can expose your entire real-time system to unauthorized access, message injection, denial-of-service, and data leakage. Run through this checklist before any production deployment.
1. Enforce TLS — Use wss:// Only
Plaintext WebSocket connections (ws://) transmit all messages in clear text. Any network observer between the client and server can read and inject messages. In production, your WebSocket endpoint must be served over wss:// (WebSocket Secure, which is WebSocket over TLS).
Configure your TLS certificates the same way you would for HTTPS. If your WebSocket server sits behind an NGINX or load balancer that handles TLS termination, the downstream connection to your application process can use plain ws:// on a private network — but the public-facing endpoint must always be wss://.
2. Validate the Origin Header
WebSockets are not subject to the browser's same-origin policy in the same way HTTP requests are. A malicious website can open a WebSocket connection to your server from any origin, and if your server accepts all connections, it may process requests from pages the user never intended to interact with.
Check the Origin header on every incoming connection during the upgrade handshake:
wss.on('connection', (socket, request) => {
const origin = request.headers.origin;
const allowed = ['https://app.example.com', 'https://www.example.com'];
if (!allowed.includes(origin)) {
socket.close(4003, 'Origin not allowed');
return;
}
});This is not a perfect defense — the header can be spoofed by non-browser clients — but it eliminates cross-site WebSocket hijacking from browser contexts, which is the primary threat.
3. Authenticate Every Connection
The WebSocket handshake is an HTTP request. You can validate session cookies or Authorization headers during the upgrade. However, many server configurations lose access to these headers after the upgrade. The more reliable pattern is the auth frame: require the client to send an authentication message immediately after connection, and close the socket if it does not arrive within a short window.
wss.on('connection', (socket) => {
const timeout = setTimeout(() => {
socket.close(4001, 'Auth timeout');
}, 5000);
socket.once('message', (raw) => {
clearTimeout(timeout);
const msg = JSON.parse(raw);
if (msg.type !== 'auth' || !verifyToken(msg.token)) {
socket.close(4001, 'Unauthorized');
return;
}
socket.userId = decodeToken(msg.token).sub;
// proceed
});
});Treat unauthenticated sockets as untrusted until the auth frame is received and verified.
4. Authorize at the Message Level, Not Just at Connection
Authentication establishes identity. Authorization determines what that identity is allowed to do. Do not assume that because a user is authenticated they can send any message type or access any room.
For every incoming message, check:
- Does the authenticated user have permission to perform this action?
- Is the resource (room, document, channel) one they have access to?
Authorization checks should live in the message handler, not only in the connection handler.
5. Rate-Limit Incoming Messages Per Connection
An authenticated user can still abuse an open connection by sending thousands of messages per second. Without rate limiting, a single malicious or buggy client can saturate your event loop.
Implement a per-connection message counter with a sliding window:
const MSG_LIMIT = 60; // max messages per minute
const msgCounts = new Map();
socket.on('message', (raw) => {
const count = (msgCounts.get(socket.id) || 0) + 1;
msgCounts.set(socket.id, count);
if (count > MSG_LIMIT) {
socket.close(4029, 'Rate limit exceeded');
return;
}
// reset counter every 60 seconds
handleMessage(raw);
});6. Limit Maximum Message Size
By default, many WebSocket libraries accept arbitrarily large messages. A client sending a 100MB payload can exhaust your server memory. Set a maximum frame size:
const wss = new WebSocketServer({
port: 3000,
maxPayload: 64 * 1024, // 64 KB max per message
});If a client sends a message exceeding the limit, the connection is terminated automatically.
7. Sanitize and Validate All Incoming Payloads
Never trust the shape or content of incoming WebSocket messages. Validate that the message conforms to the expected schema before processing it. Reject messages with unexpected fields, missing required properties, or values outside acceptable ranges.
If any message content is ultimately rendered in the browser (chat messages, document content), apply the same sanitization you would for any user-generated content in HTTP contexts.
8. Protect Against Slow Loris and Connection Exhaustion
An attacker can open thousands of connections without completing the auth handshake, consuming your server's connection budget. Defend against this by:
- Setting a hard limit on total concurrent connections
- Enforcing the auth frame timeout (covered in item 3)
- Tracking connection counts per IP and blocking IPs that open connections faster than a defined threshold
9. Close Connections Properly on Session Expiry
If a user's session expires or is revoked — because they logged out, their token was invalidated, or an admin suspended their account — any open WebSocket connections should be closed. A persistent connection that is open indefinitely regardless of session state is a security gap.
Maintain a server-side map of user ID to active sockets. When a session is invalidated, look up the user's sockets and close them with a 4001 or 4003 code.
10. Log Connection Events for Audit and Anomaly Detection
Log: connection opens and closes (with user ID, IP, and timestamp), authentication failures, rate limit violations, and oversized message attempts. These logs are the primary signal for detecting abuse patterns — credential stuffing against your WebSocket auth, coordinated connection floods, or a compromised account sending unusual message volumes.
Route these logs to your existing monitoring and alerting pipeline, not just to a file.
WebSocket security requires deliberate effort because the protocol's persistence and bidirectionality create a larger attack surface than stateless HTTP. The controls above cover the essential baseline; specific product requirements (multi-tenant isolation, compliance, regulatory logging) will add to this list.
If you are building a real-time system and want the security architecture reviewed before launch, talk to Clixo. We ship production-grade real-time products with security built in from the start.