Securing WebSockets (WSS) — Headers, CORS, and Origin Validation
Updated April 2026
Reading this? Verify your fix live. Scan your HTTP headers → HeadersFixer
WebSocket connections bypass browser CORS enforcement. The browser sends an Origin header but does not block the connection based on the server's response. Your server must validate Origin manually.
Why WebSockets bypass CORS
HTTP CORS is enforced by the browser — it checks response headers and decides whether to give JavaScript access to the response. WebSocket upgrades do not follow this model. The connection is established regardless of Origin. An attacker on a malicious site can open a WebSocket to your server using the user's cookies.
Validate Origin in your WebSocket server
Node.js (ws library)
const WebSocket = require("ws");
const ALLOWED_ORIGINS = ["https://yourapp.com", "https://staging.yourapp.com"];
const wss = new WebSocket.Server({ port: 8080, verifyClient: ({ origin, req }, callback) => { if (ALLOWED_ORIGINS.includes(origin)) { callback(true); // accept } else { callback(false, 403, "Forbidden: Origin not allowed"); } },
});
Python (websockets library)
import websockets
import asyncio
ALLOWED_ORIGINS = {"https://yourapp.com"}
async def handler(websocket, path): origin = websocket.request_headers.get("Origin", "") if origin not in ALLOWED_ORIGINS: await websocket.close(1008, "Forbidden") return # handle connection async for message in websocket: await websocket.send(f"echo: {message}")
asyncio.run(websockets.serve(handler, "0.0.0.0", 8765))
Nginx WebSocket proxy
location /ws/ { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header Origin $http_origin; # Block non-allowed origins at Nginx set $allowed_origin 0; if ($http_origin = "https://yourapp.com") { set $allowed_origin 1; } if ($allowed_origin = 0) { return 403; }
}
Always use wss:// in production
# ws:// — unencrypted, visible to network observers
# wss:// — encrypted (WebSocket over TLS)
# Frontend
const ws = new WebSocket("wss://yourapp.com/ws"); // always wss:// in production
# Nginx — terminate TLS at Nginx, proxy to ws:// backend
server { listen 443 ssl; location /ws/ { proxy_pass http://127.0.0.1:8080; # ws:// internally is fine proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; }
}
CSP for WebSocket connections
# Add wss://yourapp.com to connect-src Content-Security-Policy: default-src 'self'; connect-src 'self' wss://yourapp.com; # For development (ws:// localhost) Content-Security-Policy: connect-src 'self' wss://yourapp.com ws://localhost:*;
WebSocket authentication
// Pass token in query string (simple, visible in logs)
const ws = new WebSocket("wss://yourapp.com/ws?token=abc123");
// Or in the first message after connection (more secure)
const ws = new WebSocket("wss://yourapp.com/ws");
ws.onopen = () => { ws.send(JSON.stringify({ type: "auth", token: getToken() }));
};
WebSockets cannot send custom HTTP headers in the browser — authentication must be via query parameters, cookies, or the first WebSocket message.
Scan your HTTP headers → HeadersFixer