FlawOpen

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

● CVE-2026-71416 · CVSS 8.8 · Alta
Investigación · FlawOpen

CVE-2026-71416: secuestro de Origin WebSocket en Headroom

Headroom no validaba Origin en WebSocket; un sitio visitado podía enviar solicitudes LLM por el proxy accesible. Actualice a 0.35.0+.

💡 Explicación en Lenguaje Sencillo (ELI5)

Un recepcionista deja entrar a cualquiera a un mostrador privado porque no comprueba la credencial del visitante. Origin es esa credencial: el proxy debe validarla antes de abrir un canal que pueda usar su clave API almacenada.

Conceptos Clave y Términos

WebSocket
Conexión persistente y bidireccional que comienza con una solicitud HTTP de actualización y transporta mensajes en ambos sentidos.
Cabecera Origin
Esquema, host y puerto que el navegador envía para identificar la página que inició una solicitud. En el handshake WebSocket, el servidor debe aplicar su propia política de confianza; CORS no sustituye esta comprobación.
CSWSH
Cross-Site WebSocket Hijacking: una página no confiable abre un WebSocket hacia un servicio accesible que no valida el origen iniciador.
Credencial ambiental
Secreto disponible para el proceso, como una clave API en una variable de entorno, que puede autenticar solicitudes sin una credencial propia del cliente.
CWE-1385
Falta de validación de Origin en WebSockets: el endpoint no comprueba si el origen de comunicación está permitido.

Mapeos de marcos

Análisis de Causa Raíz

El handler WebSocket /v1/responses de Headroom aceptaba la conexión del navegador antes de comprobar Origin en el handshake. Después, el proxy podía añadir OPENAI_API_KEY de su propio entorno a la solicitud upstream. La política de mismo origen del navegador no impide por sí sola que JavaScript inicie un WebSocket a otro host accesible; el navegador suele enviar Origin, y el servidor debe rechazar los orígenes que no cumplan su política. Por tanto, la explotación requiere que el navegador de la víctima alcance un proxy Headroom vulnerable y que este tenga credenciales o integraciones útiles. El aviso indica que la conexión aceptada puede emitir solicitudes LLM arbitrarias. La ejecución de comandos en el host depende de que la solicitud pueda usar una herramienta configurada para ejecutarlos; no es una consecuencia automática de no validar la cabecera. El CNA indica que están afectadas las versiones anteriores a 0.35.0, mientras que los metadatos del aviso GitHub nombran 0.23.0. Actualice a 0.35.0 o posterior y verifique la versión instalada.

Flujo de Ataque Paso a Paso

Paso 1

El proxy conserva autoridad upstream

Un proceso Headroom puede tener una clave OpenAI en OPENAI_API_KEY; el cliente del navegador no necesita enviar esa clave.

Paso 2

El navegador visita una página no confiable

Si puede alcanzar el endpoint WebSocket de Headroom, el script de la página puede intentar conectarse. El navegador incluye el origen de la página en el handshake.

Paso 3

El handler vulnerable acepta sin política de origen

En el handler afectado, la solicitud llega a websocket.accept() sin rechazar antes un Origin no confiable. La política de mismo origen no es una autorización WebSocket del servidor.

Paso 4

El proxy reenvía una solicitud autenticada

Headroom puede completar una cabecera Authorization upstream ausente con el valor del entorno. La página del atacante puede provocar solicitudes con la autoridad upstream del proxy; el impacto depende de herramientas, credenciales, alcance de red e interacción del usuario.

Paso 5

La corrección rechaza orígenes no permitidos antes de enrutar

El parche upstream comprueba el origen antes de aceptar y reenviar la sesión. Por defecto limita los orígenes del navegador a loopback, admite orígenes configurados y cierra una conexión no permitida con el código 1008.

Código Fuente: Vulnerable vs. Seguro

El aviso identifica handle_openai_responses_ws() y la comprobación ausente antes de websocket.accept(). El commit c632023 añade _is_allowed_websocket_origin() y rechaza un origen no permitido antes del enrutamiento; conserva clientes nativos sin Origin, usa loopback como valor predeterminado para navegadores y admite orígenes configurados mediante HEADROOM_WS_ORIGINS o HEADROOM_CORS_ORIGINS. Este par breve aísla el límite de confianza modificado; no reproduce todos los auxiliares. Las pruebas upstream cubren Origin ausente, loopback, origen remoto, formato incorrecto y configuración explícita.

Fragmento de origen: headroom/proxy/handlers/openai.py

IMPLEMENTACIÓN VULNERABLE
async def handle_openai_responses_ws(self, websocket: WebSocket) -> None:
    # Patrón vulnerable: acepta la sesión del navegador antes de validar el origen.
    await websocket.accept()
    await self._relay_responses_session(websocket)
PARCHE SEGURO Y ROBUSTO
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

    # Solo una sesión del navegador aprobada por la política llega al enrutamiento upstream.
    await websocket.accept()
    await self._relay_responses_session(websocket)

Lista de Verificación de Seguridad para Ingeniería

Fuentes