# 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.

- **Published:** 2025-04-15
- **Author:** Clixo
- **Reading time:** 6 min read
- **Tags:** websockets, security, production, checklist, authentication
- **Canonical URL:** https://clixo.sh/blog/websocket-security-checklist-production

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.

```mermaid
flowchart TD
  CONN[Incoming connection] --> TLS{"wss only?"}
  TLS -->|no| REJ1[Reject]
  TLS -->|yes| ORIG{"Origin allowed?"}
  ORIG -->|no| REJ2["Close 4003"]
  ORIG -->|yes| AUTH{"Auth frame valid?"}
  AUTH -->|no| REJ3["Close 4001"]
  AUTH -->|yes| RATE{"Rate limit ok?"}
  RATE -->|exceeded| REJ4["Close 4029"]
  RATE -->|ok| PROC["Process messages (validate payload)"]
```

## 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:

```js
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.

```js
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:

```js
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:

```js
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](https://clixo.sh/#contact). We ship production-grade real-time products with security built in from the start.

---

Clixo · 1141 W Bryn Mawr Ave, Itasca, IL 60143, US · [hello@clixo.sh](mailto:hello@clixo.sh)
[Start a build](https://clixo.sh/#contact) · [All services](https://clixo.sh/services) · [Agent guide (llms.txt)](https://clixo.sh/llms.txt)
