FlawOpen

flawopen.com/Teardowns/cve-2026-71416-headroom-websocket-origin-hijacking

● CVE-2026-71416 · CVSS 8.8 · High
FlawOpen Security Research

CVE-2026-71416: Headroom WebSocket Origin Hijack

Headroom skipped WebSocket Origin checks; a visited site could relay LLM requests through its reachable proxy. Upgrade to 0.35.0+.

💡 Plain English Explainer (ELI5)

A receptionist lets anyone into a private help desk because the visitor's badge is never checked. A browser's Origin header is that badge: the proxy must check it before opening a channel that can use its stored API key.

Core Concepts & Subsystem Terms

WebSocket
A long-lived, two-way connection that begins with an HTTP upgrade request and then carries messages in both directions.
Origin header
A browser-supplied scheme, host, and port identifying the page that initiated a request. For WebSocket handshakes, the server must apply its own trust policy; CORS response headers do not replace this check.
CSWSH
Cross-Site WebSocket Hijacking: a browser on an untrusted site opens a WebSocket to a reachable service that does not verify the initiating page's origin.
Ambient credential
A secret already available to a process, such as an API key in an environment variable, which can be used by a request that lacks its own credential.
CWE-1385
Missing Origin Validation in WebSockets: a WebSocket endpoint does not verify that the communication source is allowed.

Framework mappings

Root Cause Analysis

Headroom's /v1/responses WebSocket handler accepted the browser connection before checking the handshake's Origin. The proxy could then add OPENAI_API_KEY from its own environment to the upstream request. A browser's same-origin policy does not by itself stop JavaScript from initiating a WebSocket to another reachable host; the browser normally supplies Origin, and the server must reject origins outside its policy. Exploitation therefore depends on a victim browser being able to reach a vulnerable Headroom proxy and on the proxy having useful credentials or integrations. The advisory says the accepted connection can issue arbitrary LLM requests. Host command execution is conditional on the model/API request being able to use a configured tool that executes commands; it is not an automatic consequence of the missing header check. The CVE CNA lists versions before 0.35.0 as affected, while the GitHub advisory's package metadata names 0.23.0. Upgrade to 0.35.0 or later and verify the actual installed version.

Step-by-Step Attack Flow

Step 1

A proxy holds upstream authority

A Headroom process may have an OpenAI API key in OPENAI_API_KEY; the browser client does not need to send that key itself.

Step 2

A browser visits an untrusted page

If that browser can reach the Headroom WebSocket endpoint, page script can attempt a WebSocket connection. The browser includes the page's origin in the handshake.

Step 3

The vulnerable handler accepts without origin policy

In the affected handler, the request reaches websocket.accept() without first rejecting an untrusted Origin. The usual same-origin policy is not a server-side WebSocket authorization check.

Step 4

The proxy forwards an authenticated request

Headroom can fill a missing upstream Authorization header from its environment. The attacker-controlled page can then cause requests to be made with the proxy's upstream authority; impact depends on enabled tools, credentials, network reachability, and user interaction.

Step 5

The fix rejects disallowed origins before routing

The upstream patch checks the origin before accepting and forwarding the session. It defaults browser origins to loopback, supports explicit configured origins, and closes a disallowed connection with policy-violation code 1008.

Source Code: Flaw vs. Secure Implementation

The advisory identifies handle_openai_responses_ws() and the missing check before websocket.accept(). Upstream commit c632023 adds _is_allowed_websocket_origin() and rejects a disallowed origin before the session reaches routing; it preserves native clients with no Origin, defaults browser origins to loopback, and permits configured origins through HEADROOM_WS_ORIGINS or HEADROOM_CORS_ORIGINS. This compact pair isolates the changed trust boundary; _is_allowed_websocket_origin, _header_get, logging, and the relay are existing or upstream helpers and are not fully reproduced here. The upstream regression tests cover absent Origin, loopback, remote, malformed, and explicitly configured origins.

Source excerpt: headroom/proxy/handlers/openai.py

UNPATCHED FLAW
async def handle_openai_responses_ws(self, websocket: WebSocket) -> None:
    # Vulnerable shape: accept the browser session before origin validation.
    await websocket.accept()
    await self._relay_responses_session(websocket)
HARDENED SECURE PATCH
async def handle_openai_responses_ws(self, websocket: WebSocket) -> None:
    ws_headers = dict(websocket.headers)
    if not _is_allowed_websocket_origin(ws_headers):
        logger.warning(
            "event=websocket_origin_not_allowed origin=%r",
            _header_get(ws_headers, "origin"),
        )
        await websocket.close(code=1008, reason="origin not allowed")
        return

    # Only a policy-approved browser session reaches upstream routing.
    await websocket.accept()
    await self._relay_responses_session(websocket)

Engineering & System Hardening Checklist

Sources