WritingWebSocket Security Checklist: 10 Controls Before You Ship to Production — LinkIT
5 min readwebsockets, security, production, checklist, authentication

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 connections can remain open long after the HTTP handshake. Apply authentication, authorization, payload limits, and session-expiry checks to the ongoing message flow as well as the initial request.

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;
  }
});

An explicit origin allowlist helps prevent cross-site WebSocket hijacking in browsers. Non-browser clients can forge the header, so it cannot replace authentication. See OWASP’s guidance.

3. Authenticate Every Connection

Authenticate during the handshake where the client and server support it, then retain the authenticated identity with the connection. An initial authentication message is another option; reject application messages until it succeeds and close connections that miss the authentication deadline.

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

Set an explicit message-size limit appropriate to the application. Library defaults may exceed what your service can safely accept under concurrent load:

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.


Test these controls together, including expired sessions, unauthorized room access, malformed messages, and connection floods. Add any requirements specific to your tenancy model and data.

Discuss a WebSocket security review with LinkIT.