FlawOpen

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

● CVE-2026-71416 · CVSS 8.8 · Haute
Recherche · FlawOpen

CVE-2026-71416 : détournement d'Origin WebSocket dans Headroom

Headroom ne validait pas Origin sur WebSocket ; un site visité pouvait relayer des requêtes LLM via le proxy. Passez à 0.35.0+.

💡 Explication en Langage Simple (ELI5)

Un réceptionniste laisse entrer n'importe qui dans un guichet privé parce qu'il ne vérifie pas le badge du visiteur. Origin est ce badge : le proxy doit le vérifier avant d'ouvrir un canal pouvant utiliser sa clé API stockée.

Concepts Clés et Termes

WebSocket
Connexion bidirectionnelle persistante qui commence par une requête HTTP de mise à niveau et transporte des messages dans les deux sens.
En-tête Origin
Schéma, hôte et port envoyés par le navigateur pour désigner la page à l'origine d'une requête. Pour le handshake WebSocket, le serveur doit appliquer sa propre politique de confiance ; CORS ne remplace pas ce contrôle.
CSWSH
Cross-Site WebSocket Hijacking : une page non fiable ouvre un WebSocket vers un service accessible qui ne vérifie pas l'origine initiatrice.
Identifiant ambiant
Secret déjà accessible au processus, par exemple une clé API dans une variable d'environnement, qui peut authentifier une requête sans identifiant fourni par le client.
CWE-1385
Absence de validation d'Origin dans WebSockets : le point de terminaison ne vérifie pas si l'origine de communication est autorisée.

Correspondances de référentiels

Analyse de Cause Racine

Le gestionnaire WebSocket /v1/responses de Headroom acceptait la connexion du navigateur avant de vérifier Origin dans le handshake. Le proxy pouvait ensuite ajouter OPENAI_API_KEY depuis son propre environnement à la requête upstream. La politique de même origine du navigateur n'empêche pas à elle seule JavaScript d'ouvrir un WebSocket vers un autre hôte accessible ; le navigateur transmet normalement Origin, et le serveur doit rejeter les origines hors politique. L'exploitation nécessite donc que le navigateur de la victime puisse atteindre un proxy Headroom vulnérable et que celui-ci dispose d'identifiants ou d'intégrations utiles. L'avis indique que la connexion acceptée peut émettre des requêtes LLM arbitraires. L'exécution de commandes sur l'hôte dépend de la possibilité d'utiliser un outil configuré pour exécuter des commandes ; elle ne découle pas automatiquement de l'absence de vérification de l'en-tête. Le CNA indique que les versions antérieures à 0.35.0 sont affectées, tandis que les métadonnées de l'avis GitHub citent 0.23.0. Passez à 0.35.0 ou ultérieure et vérifiez la version installée.

Déroulement de l'Attaque Étape par Étape

Étape 1

Le proxy détient une autorité upstream

Un processus Headroom peut disposer d'une clé OpenAI dans OPENAI_API_KEY ; le client navigateur n'a pas besoin d'envoyer cette clé.

Étape 2

Un navigateur visite une page non fiable

Si le navigateur peut atteindre le point de terminaison WebSocket Headroom, le script de la page peut tenter de se connecter. Le navigateur inclut l'origine de la page dans le handshake.

Étape 3

Le gestionnaire vulnérable accepte sans politique d'origine

Dans le gestionnaire affecté, la requête atteint websocket.accept() sans rejet préalable d'un Origin non fiable. La politique de même origine n'est pas un contrôle d'autorisation WebSocket côté serveur.

Étape 4

Le proxy relaie une requête authentifiée

Headroom peut renseigner l'en-tête Authorization upstream manquant depuis son environnement. La page de l'attaquant peut alors provoquer des requêtes avec l'autorité upstream du proxy ; l'impact dépend des outils, des identifiants, de l'accès réseau et de l'interaction de l'utilisateur.

Étape 5

Le correctif rejette les origines interdites avant le routage

Le correctif upstream vérifie l'origine avant d'accepter et de relayer la session. Par défaut, les origines navigateur sont limitées à loopback, les origines explicites peuvent être configurées et une connexion interdite est fermée avec le code 1008.

Code Source : Vulnérable vs Sécurisé

L'avis désigne handle_openai_responses_ws() et l'absence de contrôle avant websocket.accept(). Le commit c632023 ajoute _is_allowed_websocket_origin() et rejette toute origine interdite avant le routage ; il conserve les clients natifs sans Origin, limite par défaut les origines de navigateur à loopback et permet les origines configurées via HEADROOM_WS_ORIGINS ou HEADROOM_CORS_ORIGINS. Ce code condensé isole la frontière de confiance modifiée ; les fonctions auxiliaires ne sont pas toutes reproduites. Les tests upstream couvrent Origin absent, loopback, origine distante, valeur mal formée et configuration explicite.

Extrait source: headroom/proxy/handlers/openai.py

IMPLÉMENTATION VULNÉRABLE
async def handle_openai_responses_ws(self, websocket: WebSocket) -> None:
    # Forme vulnérable : accepter la session du navigateur avant de valider l’origine.
    await websocket.accept()
    await self._relay_responses_session(websocket)
PATCH SÉCURISÉ ET ROBUSTE
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

    # Seule une session navigateur autorisée par la politique atteint le routage amont.
    await websocket.accept()
    await self._relay_responses_session(websocket)

Liste de Contrôle de Sécurité pour l'Ingénierie

Sources