How to fix it
1. Always use wss://. The same TLS certificate that serves your HTTPS site terminates WebSocket TLS — there's no extra cost.
nginx (terminating TLS, proxying to a WebSocket app on port 3000):
location /ws {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 3600s;
}
The nginx server block is your normal HTTPS one with the cert; clients connect to wss://example.com/ws.
2. Validate the Origin header at the application level during the WebSocket handshake. The browser puts the calling page's origin in the Origin header automatically; non-browser clients can spoof it, so require an authentication token in addition for sensitive endpoints.
Node.js (ws library):
const wss = new WebSocket.Server({
server: httpsServer,
verifyClient: (info, cb) => {
const allowed = ['https://app.example.com'];
if (allowed.includes(info.origin)) cb(true);
else cb(false, 403, 'Forbidden origin');
},
});
Python (websockets library):
async def handler(ws):
if ws.request_headers.get('Origin') not in ALLOWED:
await ws.close(code=1008, reason='Forbidden origin')
return
# ... handle messages
3. Authenticate the connection. Cookies alone are not enough (because of CSWSH). Either:
- Pass an authentication token as a query parameter or first-message field, validated server-side.
- Require a CSRF token in a custom subprotocol header.
- Bind the connection to a server-issued nonce that the client must echo before any privileged action.
4. Enforce SameSite=Strict on session cookies — this also defeats CSWSH because the cookies won't be sent on a cross-origin handshake.