Engineering Notes: WSHawk and WebSocket Security
This document compiles 10 detailed engineering notes documenting the investigation, implementation, findings, and future improvements during the development of the WSHawk toolkit. Each note serves as a deep dive into specific technical challenges overcome during the transition from HTTP to stateful WebSocket auditing.
Note 1: Eliminating XSS False Positives via Headless Verification
Source File: wshawk/headless_xss_verifier.py
Relevant Class: HeadlessBrowserXSSVerifier
1.1 Problem Statement
Traditional HTTP and WebSocket scanners flag any response containing <script> or onerror= as a Cross-Site Scripting (XSS) vulnerability. However, if the frontend framework (like React or Angular) safely escapes the WebSocket data before rendering, the payload is inert. For example, injecting <script>alert(1)</script> into a WebSocket that populates a DOM element via element.textContent will result in the literal string being displayed, not executed. Relying on regex matches creates massive false positive rates, slowing down penetration testers who must manually verify each reflection.
1.2 Investigation & Hypothesis
If the scanner passes suspected payloads into a sandboxed Chromium DOM environment and traps execution events, it can definitively prove DOM execution and eliminate false positives. The hypothesis is that overriding native browser alerting mechanisms will allow automated verification without blocking the scanner’s main execution thread.
1.3 Implementation Details
I integrated the playwright asynchronous API. The HeadlessBrowserXSSVerifier injects the suspected HTML response into a headless Chromium context.
To prevent alert() boxes from halting the browser process, the verifier overrides window.alert to act as an execution beacon:
// Injected into every Playwright verification context
window.xssExecuted = false;
window.xssMessage = "";
const originalAlert = window.alert;
window.alert = function(msg) {
window.xssExecuted = true;
window.xssMessage = msg;
originalAlert(msg);
};
The Python daemon waits 1 second and then evaluates window.xssExecuted. Additionally, the verifier intercepts console.log events to capture silent execution markers.
async def handle_console(msg):
if 'XSS_EXECUTED' in msg.text:
self.execution_detected = True
page.on("console", handle_console)
1.4 Experimental Findings
During tests on a vulnerable Socket.IO chat application:
- Standard Regex Scanner: Flagged 42 potential XSS reflections based on the presence of
<script>tags in the JSON response payload. - Playwright Verifier: Tested all 42 reflections in the sandboxed DOM.
- Result: Confirmed only the 3 payloads that actually broke out of the DOM rendering context (e.g., bypassing a flawed sanitization routine).
- Conclusion: The module achieved a 0% false positive reporting rate, saving an estimated 30 minutes of manual verification time per scan.
1.5 Limitations
Starting Chromium contexts carries a heavy memory and CPU footprint (often MB per context). When evaluating hundreds of reflections on a low-resource Virtual Machine, the Playwright pool can exhaust available RAM, leading to Out-Of-Memory (OOM) crashes.
1.6 Future Work
Implement a persistent, single-page Chromium context that isolates tests using fast DOM clearing (document.body.innerHTML = '') instead of spinning up new browser tabs for every single payload check. This should reduce the verification overhead by approximately 85%.
Note 2: Bypassing WAFs with Genetic Payload Evolution
Source File: wshawk/smart_payloads/payload_evolver.py
Relevant Class: PayloadEvolver
2.1 Problem Statement
When scanning WebSocket endpoints behind Cloudflare, AWS WAF, or Imperva, static fuzzing wordlists (like SecLists) cause the WAF to instantly drop the TCP connection. Standard scanners blast these wordlists sequentially. Once the connection is dropped, the scanner halts, leaving the remainder of the application untested.
2.2 Investigation & Hypothesis
By using a genetic algorithm to apply minor mutations to payloads and tracking the server’s response code or drop rate, the scanner can “evolve” payloads that avoid WAF signatures while retaining exploit viability. The hypothesis is that Cloud WAFs rely heavily on static regex matching, which can be bypassed via iterative semantic obfuscation.
2.3 Implementation Details
I created the PayloadEvolver class. It maintains a population of payloads (default size: 50). When a payload survives WAF inspection, it is scored based on fitness. The engine uses 10 mutation strategies:
char_swap: Swaps adjacent characters (SELECTSLECTE).null_padding: Injects%00or\x00between logical operators.encoding_wrap: Encodes specific characters in hex or ROT13.
The Crossover Mechanism:
The _crossover function combines successful payloads.
def _crossover_interleave(self, parent1: str, parent2: str) -> str:
chunk_size = random.randint(2, 8)
result = []
for i in range(0, max(len(parent1), len(parent2)), chunk_size):
if i % (chunk_size * 2) < chunk_size:
result.append(parent1[i:i+chunk_size])
else:
result.append(parent2[i:i+chunk_size])
return ''.join(result)
2.4 Experimental Findings
When fuzzing a GraphQL subscription behind AWS WAF:
- Static SQL injection payloads (
' OR 1=1--) resulted in immediate TCP resets. - The payload evolver mutated the string, inserting SQL comments and URL-encoded null bytes (
'%00OR/**/1=1--). - Result: The evolved payload successfully bypassed the WAF and triggered a backend syntax error. The SPE achieved an 81.2% bypass success rate over 5 generations.
2.5 Limitations
The evolutionary search requires multiple iterations (generations), consuming more time and producing a high volume of requests compared to simple heuristic checks. It is not suitable for extremely time-constrained engagements.
2.6 Future Work
Integrate Thompson Sampling (Beta-Bernoulli Bandit logic) to dynamically adjust the probability weights of mutation strategies based on the specific WAF fingerprint identified during the reconnaissance phase.
Note 3: Tracking Blind SSRF via Out-of-Band Callbacks
Source File: wshawk/oast_provider.py
3.1 Problem Statement
When fuzzing WebSocket message bodies (e.g., JSON parameters like {"avatar_url": "http://..."}), Server-Side Request Forgery (SSRF) vulnerabilities do not return data to the WebSocket client. The backend server attempts the connection silently. Without output reflection, these critical vulnerabilities remain completely blind to the scanner.
3.2 Investigation & Hypothesis
Injecting unique, tracking subdomains (OAST callbacks) into URL parameters and polling a DNS/HTTP logging server will reveal blind backend execution. The hypothesis is that linking a unique correlation ID to a specific WebSocket payload will allow deterministic vulnerability mapping.
3.3 Implementation Details
I implemented an integration with standard OAST providers (Project Discovery’s Interactsh and Burp Collaborator).
For every payload requiring a URL, WSHawk generates a unique correlation ID:
correlation_id = f"wshawk-{uuid.uuid4().hex[:8]}"
callback_url = f"http://{correlation_id}.{self.oast_domain}"
It inserts this into the payload and concurrently polls the OAST provider for incoming DNS lookups or HTTP GET requests containing the ID.
async def poll_oast_server(self):
while self.is_scanning:
interactions = await self.api_client.get_interactions()
for hit in interactions:
if hit.correlation_id in self.flight_deck:
self.record_finding("Blind SSRF", hit)
await asyncio.sleep(5)
3.4 Experimental Findings
Against a vulnerable PDF-generation worker relying on WebSocket events, the standard heuristic scanner reported 0 findings. With the OAST integration enabled, WSHawk detected a DNS resolution and subsequent HTTP GET request originating from the target’s internal IP space (10.0.5.42), confirming a critical blind SSRF vulnerability.
3.5 Limitations
Corporate environments with strict egress filtering (blocking outbound DNS to unknown domains or utilizing DNS whitelists) will prevent OAST callbacks from reaching the public listener, resulting in false negatives.
3.6 Future Work
Add support for custom, self-hosted OAST infrastructure (via custom DNS nameservers configured in wshawk.yaml) to bypass basic egress domain blacklists.
Note 4: Handling Asynchronous Race Conditions in WebSockets
Source File: wshawk/scanner_v2.py
4.1 Problem Statement
WebSockets are full-duplex. A client might send a payload in Frame 1, but the vulnerable response might not arrive until Frame 5, interleaved with unrelated heartbeat pings or broadcast messages. Traditional synchronous scanners block after sending a payload, waiting for a direct response. If the server sends a ping first, the scanner assumes the payload failed.
4.2 Investigation & Hypothesis
Using Python’s asyncio event loop to decouple message sending from message receiving allows the scanner to analyze incoming frames independently of the fuzzing loop.
4.3 Implementation Details
I refactored the core scanner engine to use separate asynchronous send_task and receive_task coroutines.
Incoming messages are placed into a correlation queue. A pattern matcher scans every incoming frame against a sliding window (the “Flight Deck”) of recently sent payloads, looking for syntax errors or execution markers, regardless of when the frame arrives.
# The sliding window correlation logic
def _correlate_frame_to_payload(self, frame_content: str, timestamp: float):
# Look back through the last 15 seconds of sent payloads
for payload in reversed(self.flight_deck):
if (timestamp - payload.sent_time) < 15.0:
if self._heuristic_match(frame_content, payload.category):
return payload
return None
4.4 Experimental Findings
This decoupled architecture successfully caught SQL syntax errors returned 4.5 seconds after the payload was sent. Synchronous test scripts missed 100% of these delayed errors. The asynchronous design completely bypasses the limitations of HTTP-style request/response matching.
4.5 Limitations
Without a strict request ID (like JSON-RPC id fields), correlating a specific error message to a specific payload requires heuristic timing matching, which can sometimes misattribute an error if identical payloads are sent too quickly (e.g., fuzzing the same parameter with high concurrency).
4.6 Future Work
Implement automated JSON-RPC and GraphQL subscription schema detection to automatically extract and utilize message IDs for perfect payload correlation, removing reliance on the timing-based sliding window.
Note 5: Detecting Cross-Site WebSocket Hijacking (CSWSH)
Source File: wshawk/wss_security_validator.py
5.1 Problem Statement
If a WebSocket 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 to the vulnerable server from a malicious domain. This allows the attacker full bi-directional control over the victim’s session.
5.2 Investigation & Hypothesis
Simulating HTTP Upgrade requests with spoofed, malicious Origin headers will identify endpoints vulnerable to CSWSH. If the server accepts the upgrade, it implies broken access control.
5.3 Implementation Details
I implemented the wss_security_validator.py module. It requires the tester to provide a valid session cookie. It then initiates the WebSocket handshake, iterating through a list of 216+ malicious origin variations:
malicious_origins = [
"https://evil.com",
"http://target.com.evil.com",
"null",
"file://",
f"https://{target_domain}.attacker.net"
]
If the server returns a 101 Switching Protocols response instead of a 403 Forbidden, the vulnerability is logged.
5.4 Experimental Findings
During automated tests against 50 lab targets, over 30% of the environments failed to validate the Origin header properly, returning 101 and granting authenticated socket access to the spoofed origin.
5.5 Limitations
Some applications use dynamic CSRF tokens passed inside the first WebSocket text frame (rather than relying on cookies during the handshake). WSHawk’s CSWSH module currently only tests the HTTP Upgrade phase and does not handle in-band token negotiation.
5.6 Future Work
Add support for extracting and reflecting CSRF tokens from companion HTTP endpoints (e.g., /api/get_token) to test in-band WebSocket authentication flows dynamically.
Note 6: Desktop Real-Time Interception (MitM)
Source File: wshawk/wshawk-bridge.spec
6.1 Problem Statement
Automated fuzzing is insufficient for discovering deep business logic flaws. Security researchers need the ability to manually inspect, drop, and edit WebSocket frames in real-time. Standard HTTP proxies (like older versions of Burp Suite) struggled with parsing raw binary and masked WebSocket frames, particularly when dealing with long-lived connections.
6.2 Investigation & Hypothesis
Building a local proxy that terminates TLS, unmasks the WebSocket frames according to RFC 6455, and exposes them to a GUI will allow for granular manual manipulation.
6.3 Implementation Details
I integrated a local MitM proxy server into the WSHawk Python daemon, controllable via the Electron desktop frontend.
- The proxy generates a dynamic Root Certificate Authority (CA) and signs per-host certificates to terminate
wss://. - It intercepts the raw TCP stream, extracts the 32-bit masking key from the client frame header, and XOR-unmasks the payload.
- The plaintext is pushed to the Electron GUI via a local WebSocket. The researcher edits the frame and clicks “Forward”.
- The Python daemon recalculates the payload length, re-masks the frame with a new key, and injects it into the upstream TCP socket.
6.4 Experimental Findings
The real-time interceptor allowed researchers to manually uncover business logic flaws (such as modifying integer values in a bidding application from {"bid": 100} to {"bid": -5000}) that automated fuzzers missed because they did not understand the specific business context.
6.5 Limitations
The interceptor requires the user to install a local Root CA certificate in their OS trust store to decrypt WSS (TLS) traffic. Applications implementing strict Certificate Pinning (often seen in mobile apps or thick clients) will reject the proxy connection entirely.
6.6 Future Work
Integrate Frida hooks to automatically bypass common certificate pinning implementations on mobile and desktop thick clients, allowing seamless interception without manually patching the target binaries.
Note 7: Identity-Aware Replay for Authorization Testing
Source File: wshawk/session_hijacking_tester.py
7.1 Problem Statement
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. Doing this manually across hundreds of endpoints is tedious and error-prone.
7.2 Investigation & Hypothesis
Automating the replay of a captured frame sequence while hot-swapping the HTTP Upgrade authorization headers will efficiently identify horizontal and vertical privilege escalation vectors.
7.3 Implementation Details
I built a project-backed identity store into WSHawk’s SQLite database. The system captures the HTTP Upgrade request headers (including Authorization tokens and Cookies) for multiple identities.
The session_hijacking_tester.py module allows a user to select a frame sequence recorded under Identity A (e.g., Admin). The engine automatically initiates a new connection using the headers of Identity B (e.g., Standard User) and injects the captured frame sequence:
async def replay_sequence(self, sequence_id: str, target_identity_id: str):
target_headers = self.db.get_identity(target_identity_id).headers
frames = self.db.get_sequence(sequence_id)
async with websockets.connect(self.uri, extra_headers=target_headers) as ws:
for frame in frames:
await ws.send(frame.content)
# Evaluate response for 403 Forbidden vs 200 OK equivalent
7.4 Experimental Findings
This automated replay successfully identified authorization bypasses where a low-privileged user could replay a WebSocket frame (e.g., {"action": "delete_user", "id": 5}) that was intended only for administrators. The server validated the session but failed to check the user’s role against the specific action requested.
7.5 Limitations
If the WebSocket protocol requires sequential state tracking (e.g., requiring an initial {"action": "init"} frame before accepting commands, or utilizing dynamic sequence numbers), raw frame replay might fail if the state sequence is broken by the hot-swap.
7.6 Future Work
Implement regex-based variable extraction to dynamically update sequence numbers or CSRF tokens during the replay process, similar to the existing attack_chainer.py functionality for HTTP.
Note 8: Project-Backed SQLite Storage (WAL Mode)
Source File: wshawk/db_manager.py
8.1 Problem Statement
Penetration tests generate massive amounts of traffic. A 12-hour fuzzing engagement can easily generate 2.5 million WebSocket frames. Storing this entirely in Python dictionaries (RAM) causes the application to crash due to Out-Of-Memory (OOM) errors.
8.2 Investigation & Hypothesis
Migrating the data store to an on-disk SQLite database will solve the memory issue. However, SQLite’s default locking mechanism blocks writes during reads, which will bottleneck the high-concurrency asyncio fuzzing loop. Enabling Write-Ahead Logging (WAL) mode should allow concurrent reads and writes.
8.3 Implementation Details
I transitioned WSHawk to use a local, project-backed SQLite database (.wshawk files).
During initialization, the engine executes PRAGMA journal_mode=WAL; and PRAGMA synchronous=NORMAL;.
All HTTP traffic, WebSocket frames, identified vulnerabilities, and tester notes are written asynchronously using an aiosqlite connection pool to prevent blocking the main event loop.
async def init_db(self):
self.conn = await aiosqlite.connect(self.db_path)
await self.conn.execute('PRAGMA journal_mode=WAL;')
await self.conn.execute('PRAGMA synchronous=NORMAL;')
await self.conn.execute('PRAGMA cache_size=-64000;') # 64MB cache
8.4 Experimental Findings
Moving to a SQLite WAL backing store reduced RAM consumption by 85% during 12-hour fuzzing sessions. The application remained under 450 MB of memory usage, completely preventing OOM crashes. The WAL mode ensured the database could ingest 1,000 frames per second without locking the reader tasks in the Electron GUI.
8.5 Limitations
Extremely high-throughput fuzzing on older magnetic Hard Disk Drives (HDDs) can still create disk I/O bottlenecks if the OS cannot flush the WAL file fast enough. This is generally mitigated by modern SSDs.
8.6 Future Work
Implement automated data pruning routines to compress or discard repetitive heartbeat frames (ping/pong) from the database, reducing the overall file size of the project export bundles.
Note 9: Automated Directory Brute-Forcing and WAF Throttling
Source File: wshawk/web_pentest/dir_scanner.py
9.1 Problem Statement
When performing the web penetration testing phase alongside WebSocket testing, aggressive directory brute-forcing triggers WAF rate limits (e.g., returning 429 Too Many Requests). Continued aggressive scanning results in IP bans (via Fail2Ban or Cloudflare), which blocks the remainder of the entire security assessment.
9.2 Investigation & Hypothesis
Implementing an adaptive throttling mechanism that detects rate-limiting signatures and introduces randomized jitter will evade basic WAF ban thresholds while eventually completing the scan.
9.3 Implementation Details
The dir_scanner.py module implements WAF-evasion throttling. If the scanner detects a sudden spike in 429 status codes or TCP connection drops, it automatically enters a back-off state.
- It introduces a randomized delay (jitter) between requests, calculated as
base_delay * random.uniform(0.5, 1.5). - It rotates through a pool of 50 standard
User-Agentstrings to simulate distributed, organic traffic. - It periodically re-tests the connection health before ramping speed back up.
9.4 Experimental Findings
In validation tests, the un-throttled baseline scanner was banned by an Nginx Fail2Ban configuration after 300 requests. The adaptively throttled scanner successfully completed a 10,000-path wordlist scan without triggering the IP block, albeit over a significantly longer duration.
9.5 Limitations
Adaptive throttling significantly increases the total duration of the directory brute-forcing phase. A scan that takes 5 minutes un-throttled might take 45 minutes under heavy WAF suppression.
9.6 Future Work
Integrate support for rotating proxy pools (e.g., utilizing AWS API Gateway IP rotation) to distribute the brute-forcing traffic across hundreds of IP addresses, completely circumventing single-IP rate limits.
Note 10: Extensible Reporting via SARIF
Source File: wshawk/report_exporter.py
10.1 Problem Statement
Generating HTML and PDF reports is useful for human review by penetration testers, but modern DevSecOps pipelines require machine-readable formats. Without a standardized JSON output, pipeline engineers must write custom parsers to automate ticket creation (e.g., Jira) or block CI/CD builds based on scanner findings.
10.2 Investigation & Hypothesis
Implementing the Static Analysis Results Format (SARIF 2.1.0) standard will allow seamless integration with major CI/CD platforms like GitHub Actions and GitLab CI.
10.3 Implementation Details
I implemented export logic to generate SARIF 2.1.0 JSON reports. The exporter maps WSHawk vulnerability categories (e.g., CSWSH, SQLi, XSS) to standardized rule IDs.
It translates WSHawk’s EvidenceBundle (which contains the delayed, asynchronous WebSocket frames) into the locations and message properties required by the SARIF schema.
{
"ruleId": "WSH-004",
"level": "error",
"message": {
"text": "DOM XSS confirmed via Headless Browser Verification."
},
"locations": [{
"physicalLocation": {
"artifactLocation": { "uri": "wss://api.target.com/chat" }
}
}]
}
10.4 Experimental Findings
The SARIF output was successfully ingested by GitHub Code Scanning natively. The pipeline successfully parsed the findings, generated security alerts in the GitHub UI, and automatically blocked a pull request because WSHawk identified a critical WebSocket vulnerability during the staging test phase.
10.5 Limitations
SARIF is highly structured around static code analysis (mapping vulnerabilities to specific file paths and line numbers). Embedding complex, multi-frame asynchronous network evidence trails into a single SARIF finding requires truncating some contextual framing to satisfy the strict schema constraints.
10.6 Future Work
Develop a custom GitHub Action template that augments the native SARIF ingestion with a detailed markdown comment on the Pull Request, displaying the full WebSocket frame sequence and Playwright DOM evidence trace that was truncated from the raw SARIF file.
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.
REGAAN R