FlawOpen

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

● CVE-2026-71416 · CVSS 8.8 · Высокий
Исследования · FlawOpen

CVE-2026-71416: перехват WebSocket Origin в Headroom

Headroom не проверял Origin WebSocket; посещённый сайт мог отправлять запросы LLM через прокси. Обновитесь до 0.35.0+.

💡 Простыми словами (ELI5)

Охранник пропускает всех к закрытой стойке, потому что не проверяет пропуск посетителя. Origin — это такой пропуск: прокси должен проверить его, прежде чем открыть канал с доступом к сохранённому API-ключу.

Ключевые понятия и термины

WebSocket
Долговременное двустороннее соединение, начинающееся с HTTP-запроса Upgrade и передающее сообщения в обе стороны.
Заголовок Origin
Схема, узел и порт страницы, которые браузер передаёт как источник запроса. При WebSocket-handshake сервер применяет собственную политику доверия; заголовки CORS эту проверку не заменяют.
CSWSH
Cross-Site WebSocket Hijacking: недоверенная страница открывает WebSocket к доступному сервису, который не проверяет источник соединения.
Неявно доступный секрет
Секрет, уже доступный процессу, например API-ключ в переменной окружения, которым можно аутентифицировать запрос без учётных данных клиента.
CWE-1385
Отсутствие проверки Origin в WebSockets: конечная точка не проверяет, разрешён ли источник связи.

Связи со стандартами

Анализ первопричины

WebSocket-обработчик Headroom /v1/responses принимал соединение браузера до проверки Origin в handshake. Затем прокси мог добавить OPENAI_API_KEY из собственного окружения к upstream-запросу. Политика одного источника браузера сама по себе не запрещает JavaScript начать WebSocket-соединение с другим доступным узлом; обычно браузер передаёт Origin, и сервер должен отклонять источники вне своей политики. Поэтому для эксплуатации браузер жертвы должен иметь доступ к уязвимому прокси Headroom, а прокси должен располагать полезными учётными данными или интеграциями. В предупреждении сказано, что принятое соединение может отправлять произвольные запросы к LLM. Выполнение команд на хосте зависит от доступности настроенного инструмента, исполняющего команды; оно не возникает автоматически из-за отсутствия проверки заголовка. CNA указывает затронутые версии до 0.35.0, а метаданные GitHub Advisory называют 0.23.0. Обновитесь до 0.35.0 или новее и проверьте установленную версию.

Пошаговый сценарий атаки

Шаг 1

Прокси обладает полномочиями upstream

У процесса Headroom может быть ключ OpenAI в OPENAI_API_KEY; браузерному клиенту не нужно передавать сам ключ.

Шаг 2

Браузер открывает недоверенную страницу

Если браузер достигает WebSocket-точки Headroom, скрипт страницы может попытаться подключиться. Браузер указывает источник страницы в handshake.

Шаг 3

Уязвимый обработчик принимает соединение без политики источника

В затронутом обработчике запрос доходит до websocket.accept() без предварительного отклонения недоверенного Origin. Политика одного источника не является серверной авторизацией WebSocket.

Шаг 4

Прокси пересылает аутентифицированный запрос

Headroom может заполнить отсутствующий upstream-заголовок Authorization значением из окружения. Страница злоумышленника тогда вызывает запросы с полномочиями прокси; последствия зависят от инструментов, ключей, сетевой доступности и действий пользователя.

Шаг 5

Исправление отклоняет запрещённые источники до маршрутизации

Upstream-патч проверяет источник до принятия и передачи сессии. Для браузера по умолчанию разрешён loopback, нужные источники можно настроить, а запрещённое соединение закрывается с кодом 1008.

Исходный код: Уязвимый vs Защищённый вариант

В предупреждении указаны handle_openai_responses_ws() и отсутствие проверки перед websocket.accept(). Коммит c632023 добавляет _is_allowed_websocket_origin() и отклоняет запрещённый источник до маршрутизации. Нативные клиенты без Origin сохраняют совместимость, для браузера по умолчанию разрешён loopback, а явные источники задаются через HEADROOM_WS_ORIGINS или HEADROOM_CORS_ORIGINS. Сокращённый пример показывает изменённую границу доверия, но не включает все вспомогательные функции. Upstream-тесты проверяют отсутствие Origin, loopback, удалённый и некорректный источник, а также явную конфигурацию.

Фрагмент исходного кода: headroom/proxy/handlers/openai.py

УЯЗВИМАЯ РЕАЛИЗАЦИЯ
async def handle_openai_responses_ws(self, websocket: WebSocket) -> None:
    # Уязвимый вариант: принять сеанс браузера до проверки источника.
    await websocket.accept()
    await self._relay_responses_session(websocket)
БЕЗОПАСНЫЙ ИСПРАВЛЕННЫЙ ВАРИАНТ
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

    # До маршрутизации к upstream доходит только сеанс браузера, разрешённый политикой.
    await websocket.accept()
    await self._relay_responses_session(websocket)

Чек-лист по защите системы для инженеров

Источники