Back to scan results
Check 22 of 24

WebSocket Security

WebSockets are essentially TCP sockets exposed to the browser. Like HTTP, they come in plaintext (ws://) and TLS (wss://) flavors. We scan your homepage source for both and report any plaintext references.

What this check probes

  • Plaintext WebSocket references — we grep your HTML and same-origin JavaScript for ws:// URLs.
  • TLS WebSocket references — wss:// URLs are noted but not failed.
  • Common library patterns — new WebSocket(, io( (Socket.IO), SockJS, signalR, the absolute and relative URLs they accept.
  • Handshake probe — for wss:// endpoints we discover, we attempt a handshake with a deliberately wrong Origin header to see whether the server validates it.

Why this matters for PCI DSS

Two attack classes:

  • Cleartext on the wire — anyone between the user and your server (cafe Wi-Fi, ISP, hostile router) can read every WebSocket frame: chat messages, real-time order updates, sometimes session tokens or PII.
  • Cross-Site WebSocket Hijacking (CSWSH) — WebSocket handshakes do include cookies, but don't trigger CORS preflight. If your server doesn't validate the Origin header during the handshake, a malicious page can open a connection to your WebSocket from the user's browser, with the user's authenticated cookies, and exfiltrate the entire stream.

PCI DSS 4.0 Requirement 4.2.1 requires strong cryptography on any open network — ws:// on the public internet violates this. Requirement 6.2.4 covers the broader class of authentication-bypass and access-control attacks.

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.

Fixed it? Re-run the scan to confirm.

Run scan again