DEV Community

Davi
Davi

Posted on Originally published at blog.mago.team

CSWSH: Four Major WebSocket Frameworks Default to Vulnerable While Attackers Get a Bidirectional Channel

A logged-in user visits a page you do not control. Your WebSocket server opens a new authenticated connection to that page's JavaScript. The attacker reads every message your server sends and can write any message your server accepts. No XSS. No phishing. Just a missing header check.

Cross-Site WebSocket Hijacking (CSWSH) exploits a protocol-level gap in the Same-Origin Policy. The WebSocket upgrade request carries session cookies cross-origin without CORS preflight. Four of the five dominant frameworks leave Origin validation disabled by default. The consequence: a persistent, bidirectional, authenticated channel for any attacker who can load a page in the victim's browser.

The Protocol Exception That Makes CSWSH Structurally Different From CSRF

The WebSocket upgrade is an HTTP GET. Browsers execute it cross-origin without CORS preflight, carrying full session cookies. This is not a configuration error: it is an intentional protocol design decision that pre-dates modern CORS semantics.

RFC 6455 defines the handshake as an HTTP Upgrade GET. The browser sends the Origin header but no CORS restriction applies. CORS covers XMLHttpRequest and fetch(); the WebSocket() constructor bypasses both entirely, with no preflight and no Access-Control-Allow-Origin check.

The critical difference from CSRF is what the attacker receives. CSRF is blind: the attacker sends a request and cannot read the response. CSWSH establishes a persistent bidirectional channel: the attacker receives all server push messages and can send arbitrary frames as the victim.

Christian Schneider formalized the attack in 2013 under CWE-1385 (Missing Origin Validation in WebSockets). James Kettle at PortSwigger Web Security Academy is the canonical public educational reference on the mechanism. Twelve years later, the default posture across major frameworks remains unchanged.

Three CVEs That Turned Theoretical Into Catastrophic

CSWSH is not a theoretical concern. CVE-2020-25095 combined with CVE-2020-25094 produced unauthenticated RCE via WebSocket hijacking and command injection. CVE-2023-0957 produced full Gitpod account takeover. CVE-2024-51775 exposed Apache Zeppelin data to any remote attacker. All three share the same root cause: no Origin validation.

CVE-2020-25095 affected LogRhythm Platform Manager 7.4.9. The WebSocket endpoint accepted connections without Origin validation, allowing cross-site hijacking. CVE-2020-25094, disclosed alongside it, identified command injection in the Smart Response agent interface running with LocalSystem privileges. CSWSH via CVE-2020-25095 was the entry point; command injection via CVE-2020-25094 was the payload. Any attacker with a malicious page and a logged-in victim in the same browser had unauthenticated remote code execution.

CVE-2023-0957 affected Gitpod before release-2022.11.2.16. The JSONRPC API used WebSocket with cookie authentication and no Origin check. Snyk's team combined CSWSH with a SameSite subdomain bypass to achieve full workspace takeover including code execution. The fix shipped within 24 hours of disclosure.

CVE-2024-51775 affected Apache Zeppelin 0.11.1, CVSS 7.5. Remote network access, no authentication, no user interaction: any attacker with network access could read paragraph content from authenticated sessions. The pattern repeats: engineers add WebSocket to cookie-authenticated apps without mirroring the CSRF controls already on HTTP endpoints.

The common thread is engineering culture. Cookie authentication works on HTTP routes and becomes the assumed security boundary. WebSocket endpoints get added later, often by a different team, without re-examining that assumption.

Framework Defaults Scorecard: Vulnerable Out of the Box

The reason CSWSH persists across major frameworks is defaults.

Express with the ws library exposes a verifyClient callback, but it is not required. The default behavior accepts any Origin. The WebSocket upgrade bypasses the Express middleware stack entirely, requiring explicit validation inside the upgrade handler or verifyClient.

Django Channels explicitly documents the risk: "there is a risk of cross-site request forgery with WebSockets." The OriginValidator and AllowedHostsOriginValidator middlewares exist as optional ASGI wrappers. Neither is enabled in the default application setup.

FastAPI and Starlette have no built-in Origin validation for WebSocket routes. CORSMiddleware applies to HTTP routes only. Origin checks on WebSocket connections require manual code in the route handler.

Socket.io before version 2.4.0 accepted all origins by default (CVE-2020-28481). Version 2.4.0 disabled CORS by default, but this governs HTTP responses from the polling transport only. Explicit cors configuration is still required for WebSocket.

The Gorilla WebSocket library for Go defaults to comparing the Host header only. The widely copy-pasted pattern of overriding CheckOrigin with func(r *http.Request) bool { return true } to fix development errors ships to production at scale.

What Logs Show

CSWSH attempts are detectable at the upgrade boundary. The Origin header points to a domain outside the allowlist while the session cookie is valid. That combination never occurs in legitimate use.

A legitimate upgrade shows an Origin matching the app domain or a known subdomain, a valid session cookie, and a browser User-Agent. A CSWSH attempt shows an attacker-controlled domain in Origin with a valid, active session cookie attached.

The detection query is straightforward: filter upgrade requests where Origin is not in the allowlist and the Cookie header is non-empty. False positives are zero in normal operation because browsers always send the real Origin. A WAF rule equivalent flags 101 Switching Protocols responses where the preceding request's Origin is outside a curated allowlist.

Correlating the failed upgrade attempt with the active session in the authentication store confirms the specific victim account. The upgrade request's Cookie header provides the session ID directly. Most session stores record this in a retrievable format.

Blind spot: non-browser clients (curl, Postman, server-to-server) do not send an Origin header. A missing Origin should be treated as untrusted and require fallback token authentication. Silent acceptance of a missing Origin is a second attack surface.

The Three-Layer Remediation Stack

Effective defense requires all three layers simultaneously. No single layer is sufficient: server-side Origin allowlist validation, a CSRF token in the upgrade query parameter, and correctly configured SameSite cookies.

Layer 1: Origin validation on the server. Maintain an explicit allowlist of trusted origins. Reject on mismatch. Reject a missing Origin unless you have an explicit policy for non-browser clients. This control cannot be forged by in-page JavaScript.

Layer 2: CSRF token in the upgrade URL. Pass a short-lived token as a query parameter: wss://app.example.com/ws?csrfToken=.... The server validates before completing the upgrade. Generate the token per-session from the existing HTTP CSRF flow.

Layer 3: SameSite cookies. SameSite=Strict blocks all cross-site requests, including WebSocket. SameSite=Lax protects in current browsers: WebSocket handshakes are not top-level navigation requests, so browsers withhold cross-site cookies. SameSite=None remains fully exploitable. Firefox Total Cookie Protection isolates cookies by site and eliminates cookie-based CSWSH regardless of SameSite setting.

Include Security's April 2025 research found SameSite=Lax effective because WebSocket handshakes are not top-level navigation requests in current browsers. SameSite=Strict provides broader coverage for edge cases in older browsers and some mobile WebViews. Prefer Strict; Lax is effective in current browser versions but less certain in older clients.

OWASP recommends abandoning cookie-based auth for WebSockets in favor of header-based tokens (Authorization: Bearer) passed in the first message after the handshake. This approach eliminates the upgrade attack surface entirely.

The Scanner Gap and What MAGO Intel Detects

Standard DAST scanners test CSRF on POST and PUT requests. WebSocket upgrades are HTTP GETs and are systematically excluded from CSRF test suites. CSWSH remains invisible in automated security pipelines that do not explicitly target it.

The detection methodology is straightforward. Enumerate WebSocket endpoints: look for Upgrade: websocket in responses or ws:// and wss:// in JavaScript source. Send an upgrade with a forged Origin header and a valid session cookie. HTTP 101 confirms vulnerability; HTTP 400 or 403 indicates protection.

The MAGO Intel tool (intel.mago.team) includes WebSocket upgrade probes in its API surface discovery. It sends forged Origin headers against discovered ws:// endpoints and flags 101 responses with active session cookies. For each identified WebSocket endpoint, the scanner validates whether the server distinguishes between trusted and attacker-controlled origins.

Burp Suite Professional maintains a WebSocket history tab and includes CSRF scanner extensions. Automated CSWSH testing requires a custom check or the CSRF Scanner extension configured for Upgrade requests.

The fix is two lines in your WebSocket server config. Most teams apply CSRF controls to the API endpoints they think about and leave the WebSocket handshake unguarded. That handshake is the one endpoint that opens a persistent authenticated channel. Audit your upgrade handlers before your next penetration test does.

Top comments (0)