Case Study: Stateful Security Auditing of WebSocket Applications

This case study analyzes the architecture, engineering, and deployment of WSHawk v4.0.4. It documents the transition from standard stateless HTTP fuzzing to stateful, asynchronous WebSocket vulnerability testing, and details the implementation of Smart Payload Evolution (SPE) and browser-assisted evidence collection.


1. Introduction

Modern web architecture has undergone a fundamental shift over the past decade. The traditional request-response model, dominated by stateless HTTP/REST APIs, is increasingly being replaced or augmented by persistent, full-duplex communication protocols like WebSockets (RFC 6455). Financial trading platforms, collaborative document editors, live customer support chats, multiplayer gaming backends, and IoT telemetry dashboards now rely on WebSockets to push data to clients instantly without the overhead of HTTP polling.

Despite this massive architectural shift, the landscape of offensive security tooling remains heavily biased toward HTTP. The vast majority of open-source and commercial web vulnerability scanners (e.g., Nikto, standard Burp Suite active scans, OWASP ZAP’s default spiders) struggle to effectively evaluate WebSocket infrastructure.

This case study details the engineering journey of building WSHawk, a Python and Electron-based security testing framework designed to audit stateful WebSocket connections, adapt payloads to observed responses, and strengthen findings with headless-browser evidence.


2. The Limits of HTTP Scanners

To understand why a dedicated WebSocket scanner is necessary, we must analyze the structural limitations of HTTP-centric fuzzing tools.

2.1 State Persistence and Asynchrony

HTTP is inherently stateless. A scanner sends Request A (containing a payload) and immediately receives Response A. If Response A contains a SQL error, the scanner definitively maps the vulnerability to Payload A.

WebSockets break this paradigm. A WebSocket connection is established once (via the HTTP Upgrade handshake) and remains open.

  1. The scanner sends Payload A in Frame 1.
  2. The server might respond with a heartbeat ping in Frame 2.
  3. The server might broadcast a chat message from another user in Frame 3.
  4. Finally, the backend database worker finishes executing Payload A and returns a SQL syntax error in Frame 4, five seconds after the payload was sent.

An HTTP scanner that blocks waiting for a synchronous response will read Frame 2 (the ping), conclude that Payload A failed, and move on. It will completely miss the critical vulnerability exposed in Frame 4.

2.2 Protocol Opacity and Framing

HTTP responses include standardized status codes (200 OK, 403 Forbidden, 500 Internal Server Error). Scanners rely heavily on these codes to heuristically determine the success of an attack (e.g., detecting a 500 error during fuzzing usually indicates unhandled input).

WebSocket frames do not carry HTTP status codes. A payload that completely crashes a backend database might result in a generic WebSocket text frame containing {"status": "error", "code": 9001} or simply a silent connection drop. Scanners that cannot parse the specific JSON schema or binary framing of the target application are effectively blind.

2.3 The False Positive Dilemma in XSS

When an HTTP scanner injects <script>alert(1)</script> into a parameter and sees the exact string reflected in the HTTP response, it flags a Cross-Site Scripting (XSS) vulnerability.

In modern single-page applications (SPAs) built with React, Vue, or Angular, data received over a WebSocket is rarely injected directly into the DOM using innerHTML. It is typically bound to the state and rendered safely using textContent. Thus, the payload reflects in the network traffic, but it is inert in the browser. Regex-based scanners flag this as a critical vulnerability, generating massive volumes of false positives that waste the time of penetration testers and triage engineers.


3. Threat Model

WSHawk operates under a strictly defined threat model targeting the OWASP Top 10 API Security and Web Application risks within the context of full-duplex communication.

3.1 In Scope

  • Cross-Site WebSocket Hijacking (CSWSH): Exploiting endpoints that rely solely on ambient credentials (cookies) without validating the Origin header or requiring CSRF tokens during the handshake.
  • DOM & Reflected XSS: Exploiting unsafe client-side rendering of WebSocket frames.
  • Blind SSRF & OAST: Triggering out-of-band network calls from backend workers processing message queues fed by WebSocket data.
  • Injection (SQLi/NoSQLi): Exploiting backend database queries triggered by specific message properties.
  • Business Logic Flaws: Identifying race conditions and horizontal privilege escalation (IDOR) through Identity-Aware Replay.

3.2 Out of Scope

  • Volumetric DDoS: Attacks designed to exhaust network bandwidth or connection pools (e.g., Slowloris style socket exhaustion).
  • Zero-Day Binary Exploitation: Exploiting buffer overflows in the underlying web server software (e.g., Nginx, Apache, IIS).
  • Physical Security and Social Engineering: Out of scope for network assessment tools.

4. Architecture and Design Decisions

WSHawk is architected as a hybrid platform. The core fuzzing and interception engine runs as a headless Python daemon, leveraging Python’s strong ecosystem for asynchronous networking (asyncio, websockets) and browser automation (playwright). The visualization layer is an Electron desktop application that communicates with the Python daemon via local REST APIs and WebSockets.

4.1 The Asynchronous Event Loop (asyncio)

To solve the state persistence problem (Section 2.1), WSHawk decouples message transmission from message reception.

In the scanner_v2.py module, the engine initializes two primary coroutines that run concurrently on the asyncio event loop:

1. The Send Task: This task consumes payloads from the PayloadEvolver queue and injects them into the socket. Crucially, it records the exact timestamp, payload content, and mutation strategy into a sliding window memory structure (the “Flight Deck”).

2. The Receive Task: This task continuously listens to the socket. It parses every incoming frame and applies heuristic pattern matching. It uses compiled regex engines to search for standard database engine complaints (e.g., MySQL SQL syntax, Oracle ORA-[0-9]{4}, PostgreSQL PostgreSQL query failed).

Correlation Logic: When the Receive Task detects an error marker in Frame N, it queries the Flight Deck. It correlates the error to the payload injected within the appropriate timing window (e.g., T5sT_{-5\text{s}} to T0sT_{0\text{s}}).

# Simplified Representation of the Async Fuzzing Loop
async def send_task(self):
    while not self.fuzz_queue.empty():
        payload = await self.fuzz_queue.get()
        await self.websocket.send(payload.content)
        self.flight_deck.append({
            'payload': payload.content,
            'timestamp': time.time()
        })
        await asyncio.sleep(self.delay)

async def receive_task(self):
    async for message in self.websocket:
        if self._matches_error_heuristic(message):
            matched_payload = self._correlate_to_flight_deck(message)
            self._record_vulnerability(matched_payload, message)

This decoupled architecture lets WSHawk continue sending within configured limits while it captures delayed, asynchronous responses. Actual throughput depends on the target, transport, payloads, browser verification, rate limits, and host resources.


5. The Smart Payload Evolver (Deep Dive)

Perhaps the most significant engineering challenge in web penetration testing is bypassing Web Application Firewalls (WAFs). Cloud WAFs (Cloudflare, AWS WAF, Akamai) easily fingerprint static fuzzing behavior. If a scanner blasts 1,000 payloads from SecLists, the WAF detects the malicious regex signatures, drops the TCP connection, and bans the IP address.

To bypass this limitation, WSHawk implements the Smart Payload Engine (SPE). The SPE models payload discovery as a genetic optimization problem, implemented in wshawk/smart_payloads/payload_evolver.py.

5.1 The Genetic Algorithm

Instead of testing 10,000 static strings, the PayloadEvolver maintains a small “population” of payloads (default: 50). It tracks the success of each payload using a fitness score.

When a payload bypasses a WAF but fails to trigger an exploit (e.g., returning a valid JSON response but no SQL error), the engine assigns it a moderate fitness score and uses it as a parent for the next generation.

5.2 Mutation Strategies

The engine randomly applies techniques to alter the string structure without destroying its exploit capability.

  • null_insert: Injects URL-encoded or raw null bytes (%00, \x00).
  • case_change: Swaps capitalization to break case-sensitive regex.
  • encoding_wrap: Encodes random characters using Base64, Hexadecimal, or ROT13.
  • duplicate_segment: Duplicates chunks of the payload to confuse buffer-based filters.

Example Mutation Implementation:

def _mutate_case_change(self, payload: str) -> str:
    # Randomly select a case mutation strategy
    func = random.choice([
        lambda s: s.swapcase(),
        lambda s: ''.join(c.upper() if i%2 else c.lower() for i,c in enumerate(s)),
        lambda s: s.upper()
    ])
    return func(payload)

5.3 Crossover Strategies

When two payloads perform well, the engine breeds them using crossover recombination.

  • interleave: Slices both parents into chunks and alternates them.
  • split: Takes the first half of one and the second half of the other.
  • wrap: Nests one payload entirely inside the other.

5.4 Fitness Scoring

Payloads are hashed using MD5 to prevent evaluating duplicate strings in subsequent generations. The engine uses an exponential moving average to update fitness:

Fitnessnew=0.4×Score+0.6×Fitnessold\text{Fitness}_{\text{new}} = 0.4 \times \text{Score} + 0.6 \times \text{Fitness}_{\text{old}}

Payloads surpassing a 0.70.7 fitness threshold enter the “Hall of Fame”, producing highly specific, undocumented bypass strings tailored to the specific target environment.

5.5 Controlled Evolution Workflow

SPE treats payload mutation as a feedback-driven search. A seed can be encoded, padded, case-shifted, or crossed with another candidate, then rescored using target observations such as blocking, reflection, timing, and error changes. The important engineering property is reproducibility: the run records the seed, transformations, target context, and resulting evidence.

An evolved payload is not automatically an exploit. A changed response can indicate a WAF difference, parser behavior, application error, or noise. WSHawk therefore keeps the mutation history and requires protocol-specific evidence or operator confirmation before a result is reported as a vulnerability.


6. Playwright Headless Verification (Deep Dive)

As discussed in Section 2.3, reflection-only XSS findings can be noisy. WSHawk can pass suspected reflections to a Chromium instance controlled through Playwright to collect stronger execution evidence.

6.1 Sandboxed DOM Verification

Implemented in wshawk/headless_xss_verifier.py, the HeadlessBrowserXSSVerifier spins up an isolated Chromium page when the engine detects a reflected payload.

6.2 Beacon Injection

It overrides the native browser alert function to trap execution without halting the test or requiring user interaction.

// Injected DOM tracking beacon
window.xssExecuted = false;
const originalAlert = window.alert;
window.alert = function(msg) {
    window.xssExecuted = true;
    window.xssMessage = msg;
    originalAlert(msg);
};

// Check for DOM-based execution
setTimeout(() => {
    if (window.xssExecuted) {
        console.log('XSS_EXECUTED: ' + window.xssMessage);
    }
}, 100);

6.3 DOM Mutation Tracking

In addition to trapping alert(), the verifier queries the DOM for structural mutations caused by the payload. It counts injected <script> tags and scans the DOM tree for inline event handlers (onerror, onmouseover) that shouldn’t be present.

// Checking for injected event handlers
const elements = document.querySelectorAll('*');
for (let el of elements) {
    for (let attr of el.attributes) {
        if (attr.name.startsWith('on')) {
            return true; // Malicious handler detected
        }
    }
}

If the beacon fires or a relevant DOM mutation is observed, the result gains browser-assisted evidence. If the browser renders the payload harmlessly, reflection alone is not treated as proof of execution. This reduces uncertainty but does not promise a universal false-positive rate because production state, timing, and browser policy may differ from the replay context.


7. Real-Time Interception (MitM Proxy)

Offensive security engineers rely heavily on interception proxies (like Burp Suite or OWASP ZAP) to pause, edit, and forward traffic. While HTTP interception is standard, manipulating WebSocket frames in real-time presents challenges due to binary masking (RFC 6455) and connection persistence.

7.1 TLS Termination and Unmasking

WSHawk Desktop integrates a custom Man-in-the-Middle (MitM) interceptor. The Python bridge generates a dynamic Root Certificate Authority (CA) and signs per-host certificates on the fly. This terminates the wss:// TLS connection locally, allowing WSHawk to read the plaintext traffic.

RFC 6455 requires all client-to-server WebSocket frames to be masked via a 32-bit masking key to prevent cache poisoning. The MitM proxy intercepts the raw TCP stream, XOR-unmasks the payload using the 32-bit key, and exposes the plaintext text or binary JSON to the user interface.

7.2 Manual Frame Editing

The interceptor pauses the event loop, displaying the unmasked frame in the GUI. The engineer can modify parameters (e.g., changing {"role": "user"} to {"role": "admin"}) and click “Forward”. The proxy re-masks the modified frame, calculates the new payload length, and injects it into the upstream TCP socket.

This enables deep business logic testing that automated heuristics cannot detect.


8. Identity-Aware Replay for Authorization Testing

Testing for Insecure Direct Object References (IDOR) and broken access control in WebSockets requires sending a payload as User A, and then re-sending the exact same payload sequence as User B to see if access is granted.

8.1 Implementation

I built a project-backed identity store into WSHawk (wshawk/session_hijacking_tester.py). The system captures the HTTP Upgrade request headers (including Authorization tokens and Cookies) for multiple identities during the reconnaissance phase.

The session_hijacking_tester.py module allows a user to select a sequence of frames recorded under Identity A (e.g., an administrator performing a privileged action) and automatically replay it using the handshake headers of Identity B (a standard user).

If the server processes the privileged commands for the low-privileged identity, it flags a horizontal or vertical privilege escalation vulnerability.


9. Tracking Blind SSRF via Out-of-Band Callbacks

Some of the most critical vulnerabilities—like Server-Side Request Forgery (SSRF), XML External Entity (XXE) injection, and blind command injection—do not return output to the client. A payload exploiting a backend webhook processor might succeed perfectly, but the WebSocket frame returned to WSHawk merely says {"status": "processing"}.

9.1 OAST Integration

To detect these flaws, WSHawk integrates an Out-Of-Band (OAST) callback provider mechanism (wshawk/oast_provider.py).

  1. Payload Generation: The engine generates unique correlation IDs. Instead of injecting http://localhost, it injects a unique domain like http://wshawk-9f8a2c.collaborator-server.net.
  2. Execution: The payload is sent over the WebSocket. The backend server parses the payload and, if vulnerable to SSRF, performs a DNS lookup or HTTP GET request to the unique domain.
  3. Polling and Correlation: Concurrently, WSHawk polls the OAST provider’s API. If the provider logs a DNS or HTTP hit for wshawk-9f8a2c, WSHawk correlates that exact ID back to the specific WebSocket payload and timestamp.

This enables WSHawk to map internal backend architectures and confirm critical blind vulnerabilities that stateless, synchronous scanners miss entirely.


10. Project-Backed Storage

Long-running assessments need durable state. WSHawk stores projects, targets, identities, HTTP flows, WebSocket frames, evidence, and findings in local project storage so operators can pause, replay, compare identities, and export evidence without keeping every object in renderer memory.

The repository uses safe migrations and backup-aware storage paths. No public 12-hour memory or frames-per-second study is currently claimed; storage performance should be measured with the checked-in benchmark harness on the release and platform being evaluated.


11. Ground-Truth Validation

WSHawk v4.0.4 includes repeatable local validation rather than vendor-specific marketing benchmarks:

  • 26 paired HTTP/WebSocket cases in the standalone desktop security lab;
  • vulnerable and hardened controls for authorization, SSRF, redirects, CORS, sensitive data, CSRF, races, subscriptions, and WebSocket Origin policy;
  • 34 authorization scenarios in the Electron + Go lab across HTTP, GraphQL, and WebSocket controls;
  • warm-up exclusion, fresh target state for each measured iteration, correctness gates, timing statistics, and redacted machine-readable reports.

The harness is intentionally a functional and regression benchmark. It does not claim a universal attack-success rate, a maximum-throughput figure, or perfect detection on arbitrary applications. See the dedicated Benchmarks page for commands and scope.


12. Cross-Site WebSocket Hijacking (CSWSH) Detection

A unique vulnerability to WebSockets is CSWSH. If an endpoint relies solely on session cookies for authentication and fails to validate the Origin header during the HTTP Upgrade handshake, an attacker can trick a victim’s browser into opening an authenticated WebSocket connection from a malicious domain.

WSHawk automates this check via the wss_security_validator.py module. It initiates the WebSocket handshake providing the target’s valid session cookie but injects a list of 216+ malicious origin variations:

  • Origin: https://evil.com
  • Origin: null
  • Origin: https://target.com.evil.com

If the server returns a 101 Switching Protocols response instead of a 403 Forbidden, WSHawk flags the endpoint as vulnerable to hijacking, allowing testers to easily identify severe cross-domain trust violations.


13. Limitations and Edge Cases

While WSHawk represents a significant leap in stateful auditing, it has known limitations:

  1. Playwright Overhead: Launching Chromium contexts requires significant CPU and memory resources (often >200>200MB per context), slowing down the scanner on low-resource VMs when verifying hundreds of reflections.
  2. Proprietary Binary Frames: While WSHawk supports raw binary frames, proprietary binary serialization formats (like custom Protobuf implementations without available schemas) limit the mutator’s ability to inject structurally sound payloads. The fuzzer falls back to random byte-flipping, which is highly inefficient.
  3. Application-Layer Rate Limiting: High-frequency WebSocket fuzzing will trigger application-layer rate limits if the backend tracks messages per connection session (e.g., allowing only 5 chat messages per second). The tester must manually lower the --concurrency flag to avoid account lockouts.

14. Future Work

Future releases of the WSHawk toolkit will focus on addressing these limitations:

  • Persistent Browser Contexts: Implementing a persistent, single-page Chromium context that isolates XSS tests using fast DOM clearing (document.body.innerHTML = '') instead of spinning up new tabs, reducing memory overhead by 90%.
  • Protobuf Schema Importer: Adding deep integration with custom Protobuf schemas, allowing the engine to deserialize binary frames into JSON, mutate the specific string fields, and re-serialize them into structurally sound binary payloads.
  • Tauri Migration: Migrating the Electron frontend to a lighter weight Tauri (Rust) desktop framework to reduce the baseline memory footprint of the graphical user interface.

15. Conclusion

WSHawk v4.0.4 demonstrates why WebSocket assessment benefits from stateful, asynchronous evaluation. HTTP-only request/response workflows do not fully model delayed server messages, subscriptions, identity-aware replay, or cross-frame application behavior.

By integrating a multi-armed genetic mutator with sandboxed browser DOM verification and OAST callbacks, WSHawk provides offensive engineers with an automated, highly accurate framework to map and exploit the blind spots of modern real-time applications. The transition to an asyncio decoupled core and SQLite WAL storage ensures the framework scales to handle multi-day enterprise penetration testing engagements without compromising data integrity or system stability.


Reproducibility

The commands, configuration, and methodology described above are intended to allow independent verification. Target application configurations (like WAF rules and Rate Limits) may cause varying results when reproducing tests in external environments.