Меняю заголовок на
Host: target.internal/health?x= - тот же запрос, тот же HTTP-путь, но в ответе 200 OK с полным содержимым административной панели. Один символ / в значении Host обрушивает модель авторизации целиком. Это CVE-2026-48710 - Host Header Injection уязвимость в Starlette, ASGI-фреймворке под капотом FastAPI, которую CISA внесла в каталог активно эксплуатируемых уязвимостей (KEV) 2 сентября 2026 года с дедлайном устранения 16 сентября. Исследователи назвали её BadHost. И затрагивает она не только классические веб-приложения - под ударом инфраструктура AI-инференса на Python: vLLM, LiteLLM, MCP-серверы и сотни тысяч зависимых проектов.Два пути в одном запросе: корень FastAPI уязвимости
Starlette - легковесный ASGI-фреймворк, на котором построены FastAPI и десятки AI-инструментов. Внутри него при обработке HTTP-запроса живут два отдельных представления пути. И вот тут начинается самое интересное. Подробнее - в нашем подробном разборе безопасность llm приложений.Сырой ASGI-путь - значение
scope["path"], извлечённое напрямую из строки запроса (GET /admin HTTP/1.1). Роутер Starlette использует именно его для выбора обработчика эндпоинта.Реконструированный URL - объект
request.url, который Starlette собирает конкатенацией схемы (http/https), содержимого заголовка Host и пути. Разработчикам доступен как request.url.path.В штатном режиме значения совпадают. Запрос
GET /admin с Host: example.com даёт scope["path"] = /admin и request.url.path = /admin. Разработчики считают их взаимозаменяемыми - и пишут middleware авторизации через request.url.path, не подозревая о расхождении.До версии 1.0.1 Starlette не валидировал содержимое заголовка
Host перед подстановкой в URL. Символы /, ? и # - структурные разделители в URI (путь, query string, фрагмент). Попав в позицию hostname при конкатенации, они сдвигают границы между компонентами URL при повторном парсинге. По данным GitHub Security Advisory GHSA-86qp-5c8j-p5mr, это и есть корневая причина BadHost.Конкретный механизм расхождения. Атакующий отправляет запрос:
Код:
GET /admin HTTP/1.1
Host: example.com/health?x=
http://example.com/health?x=/admin. При парсинге этой строки request.url.path становится /health, а ?x=/admin уходит в query string. Роутер работает с scope["path"] = /admin и вызывает обработчик защищённого эндпоинта.Middleware видит
/health (публичный маршрут) - пропускает. Роутер выполняет /admin. Никакой криптографии, никакого подбора токенов - чистое расхождение интерпретаций того, какой ресурс защищается. Классический time-of-check / time-of-use внутри одного стека обработки.CVSS v3.1: 6.5 MEDIUM, вектор
AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N - эксплуатация по сети, не требует ни привилегий, ни действий пользователя. Классификация: CWE-444 (Inconsistent Interpretation of HTTP Requests) и CWE-1289 (Improper Validation of Unsafe Equivalence in Input). Оба CWE про одно: уязвимость не в отдельном компоненте, а на стыке их интерпретаций.Практическая эксплуатация BadHost атаки
📚 Часть контента скрыта. Этот материал доступен участникам сообщества с рангом One Level или выше
Получить доступ просто — достаточно зарегистрироваться и проявить активность на форуме
Получить доступ просто — достаточно зарегистрироваться и проявить активность на форуме
,
/metrics. Проверяем: curl -v http://target:8000/health - ответ 200 OK.Шаг 3. Инъекция в Host header. Подставляем публичный маршрут так, чтобы
request.url.path стал равен ему:curl --http1.1 -v -H "Host: target:8000/health?x=" http://target:8000/adminУязвимое приложение вернётПримечание: команда протестирована с curl 8.x по HTTP/1.1. Некоторые версии curl/libcurl могут валидировать или ре-эскейпить значение Host - при ложноотрицательном результате используйте raw-сокет или Burp Repeater.
200 OK с содержимым /admin. Демонстрация X41 D-Sec (исследователи, обнаружившие BadHost) воспроизводит именно этот паттерн - по данным CSO Online, «They sent the same request with one extra character in the Host header, and the page returned a 200 OK». Один символ - и 403 превращается в 200. Красота.Шаг 4. Вариации payload. Кроме
/ работают ? и #, и разные URL-парсеры обрабатывают их по-разному:Host: target:8000/docs?- маскировка под документацию FastAPIHost: target:8000?/health#- сдвиг через query stringHost: target:8000#/public- сдвиг через фрагмент
/health?, /docs?, /public?, ?/, #/. Grep по status code 200 выявляет успешные обходы. Для blind-сценариев (одинаковое тело ответа) сравниваем длину response body и наличие заголовков аутентификации (X-Auth-User, Set-Cookie) - их появление или исчезновение подтверждает расхождение путей.SSRF через Host заголовок в AI-инфраструктуре
Host Header Injection уязвимость в Starlette открывает вектор не только обхода авторизации. Если приложение используетrequest.url для формирования исходящих запросов (callback URL, webhook-нотификации, redirect после OAuth), атакующий контролирует целевой хост - прямой SSRF. По MITRE ATT&CK это Exploit Public-Facing Application (T1190, Initial Access).Для AI-инфраструктуры сценарий особенно неприятен. LLM-gateway принимает запрос, извлекает
request.url.scheme и request.url.netloc для проксирования к model-serving бэкенду. Подменённый Host перенаправляет запрос на внутренний сервис - metadata API облака (169.254.169.254) для получения IAM-креденшалов, Redis с кэшированными API-ключами или model endpoint без аутентификации.Бизнес-логика атаки: злоумышленник получает доступ к дорогостоящим GPU-ресурсам инференса, крадёт API-ключи для перепродажи доступа к коммерческим моделям или - в случае MCP-серверов - вызывает произвольные инструменты от имени агента. Это покрывается OWASP A10:2021 (Server-Side Request Forgery) и A05:2021 (Security Misconfiguration).
Цепочка CVE-2026-48710 + CVE-2026-42271: от Starlette bypass до RCE
CISA в записи KEV отмечает, что CVE-2026-48710 может быть объединена (chained) с CVE-2026-42271. На практике - эскалация от обхода авторизации до выполнения произвольного кода на хосте.CVSS 8.7 HIGH (CVSS v4.0). Два эндпоинта для тестирования MCP-серверов (
POST /mcp-rest/test/connection и POST /mcp-rest/test/tools/list) принимали конфигурацию с полями command, args и env для stdio-транспорта. При подключении LiteLLM выполнял указанную команду - CWE-77 и CWE-78 (Command Injection). CISA KEV: добавлена 8 июня 2026, дедлайн 22 июня 2026. Затронуты litellm:litellm и Red Hat OpenShift AI.Цепочка атаки разворачивается в три шага:
- BadHost (CVE-2026-48710) обходит авторизацию на LiteLLM-прокси - запрос с подменённым Host попадает к административным эндпоинтам
- Атакующий вызывает
POST /mcp-rest/test/connectionс конфигурацией stdio-транспорта и произвольной командой - LiteLLM порождает процесс - полноценный RCE на хосте шлюза
learner202649/CVE-2026-42271-PoC на GitHub.CISA не добавляет уязвимости в KEV по формальному CVSS - добавление означает подтверждённую эксплуатацию в дикой природе. Если попало в KEV - кто-то уже зашёл.
Уязвимость vLLM и MCP-серверов: кто под ударом
По данным X41 D-Sec (обнаруживших BadHost при аудите vLLM, организованном OSTIF и спонсированном Alpha-Omega Project), Starlette имеет более 400 000 зависимых проектов на GitHub. Не все уязвимы - но три группы с максимальным риском выделяются особо.FastAPI/Starlette-приложения без обратного прокси. Dev-серверы, staging, внутренние API. Uvicorn с
--host 0.0.0.0 без nginx - типичная конфигурация при разработке и у команд, которые про reverse proxy в продакшне «ну, потом прикрутим».Model-serving и AI-gateway. vLLM, LiteLLM, OpenAI-compatible proxy, evaluation dashboards. По данным CSO Online, «Research, evaluation and development setups for AI software often do not [have a reverse proxy], and many run the application server facing the network directly». ML-инженеры деплоят быстро - безопасность AI-агентов при этом уходит на второй план.
MCP-серверы. Спецификация Model Context Protocol требует наличия неаутентифицированных OAuth discovery endpoints - это даёт атакующему гарантированную точку входа для эксплуатации BadHost без необходимости угадывать публичные маршруты. Подарок прямо из спеки.
Затронутые продукты по данным NVD: encode:starlette, Red Hat AI Inference Server, Red Hat Ansible Automation Platform, Red Hat Migration Toolkit for Applications.
Существенный нюанс: команда может не подозревать о наличии уязвимой Starlette. Зависимость приходит транзитивно через FastAPI. Проверка:
pip show starlette | grep Version. В контейнерных деплоях: docker run --rm <image> pip show starlette. Это категория OWASP A06:2021 (Vulnerable and Outdated Components) - уязвимый компонент, о котором разработчик даже не знает.Обход валидации хоста: детектирование и патч
Индикаторы эксплуатации в логах
Ключевой признак попытки эксплуатации - URI-значимые символы/, ?, #, @ в значении заголовка Host. В access-логах nginx или uvicorn:grep -P 'Host:\s[I][^,\s][/I][/?#@][^,\s]*' /var/log/access.logДля SIEM: правило по полю
http.request.header.host с regex, ловящим символы, недопустимые в hostname по RFC 9112.Второй индикатор - защищённые маршруты, возвращающие неожиданный
200 OK. Если /admin должен отвечать 401/403, а в логах появляется 200 - смотрите соответствующий запрос на предмет модифицированного Host.ProjectDiscovery выпустили nuclei-шаблоны для обеих CVE:
CVE-2026-48710.yaml и CVE-2026-42271.yaml. Массовая проверка инфраструктуры одной командой: nuclei -t http/cves/2026/CVE-2026-48710.yaml -l targets.txt. Исследователи X41 D-Sec также создали сервис badhost.org для проверки конкретных хостов.Патч Starlette 1.0.1 и верификация
Starlette 1.0.1 добавляет валидацию заголовка Host - значения с URI-значимыми символами отклоняются до реконструкции URL. Обновление:pip install "starlette>=1.0.1". Для FastAPI - убедитесь, что транзитивная зависимость действительно обновилась (проверить pip freeze | grep starlette и pinned-версии в requirements.txt / poetry.lock).Верификация после патча: отправляем запрос с инъекцией и проверяем HTTP-код ответа:
curl -s -o /dev/null -w "%{http_code}" -H "Host: target/health?" http://target:8000/adminНа патченной версии ожидается
400 Bad Request (невалидный Host отклонён) или тот же 403/401, что и без инъекции. Ответ 200 - патч не применён.Reverse proxy - defence-in-depth, не замена патча. Конфигурация nginx должна содержать явный
default_server для отклонения запросов к неизвестным хостам:
NGINX:
server {
listen 80 default_server;
return 444;
}
Чек-лист для баг-баунти отчёта по инъекции HTTP заголовка
| Элемент отчёта | Содержание |
|---|---|
| Название | Host Header Authentication Bypass via CVE-2026-48710 (BadHost) |
| Severity | Medium (6.5 base), повышается при chaining |
| Preconditions | Starlette < 1.0.1, нет compliant reverse proxy, auth через request.url.path |
| Steps to reproduce | curl с модифицированным Host, скриншоты 403 → 200 |
| Impact | Обход авторизации, доступ к защищённым эндпоинтам |
| Chaining | CVE-2026-42271 (LiteLLM RCE), SSRF через контролируемый Host |
| Remediation | Starlette >= 1.0.1 + reverse proxy + переход на scope["path"] в middleware |
| References | GHSA-86qp-5c8j-p5mr, CISA KEV |
При chaining с CVE-2026-42271 severity в отчёте обосновывается как Critical - демонстрация полной цепочки от одного HTTP-запроса до RCE существенно повышает bounty. В отчёте важно показать именно переход: один запрос → обход auth → второй запрос → выполнение команды. Программы вроде HackerOne любят такие цепочки - это не «теоретический Medium», а практический Critical с PoC.
Восемь лет эта уязвимость жила в Starlette. Не нашли SAST-сканеры, не нашёл fuzzing HTTP-парсеров, не поймал code review с помощью LLM - по отзывам исследователей, баг оказался «too subtle for current Anthropic models». Нашли руками X41 D-Sec при аудите vLLM - и обнаружили проблему не в vLLM, а тремя уровнями зависимостей глубже.
Каждый компонент в изоляции ведёт себя разумно: роутер берёт путь из scope, URL builder склеивает Host с path, middleware проверяет URL. Баг рождается на стыке интерпретаций.
pip audit ловит known CVE в прямых зависимостях, но не отвечает на вопрос: «что мой middleware делает с заголовком, который уже прошёл через три слоя парсинга?».Таких уязвимостей «на стыке» в AI-стеке будет больше - MCP-серверы, agent-фреймворки, model-gateway собираются из десятков библиотек, у каждой собственная модель валидации входных данных. Не автоматизированный аудит зависимостей решит эту проблему, а ручной разбор interaction boundaries между компонентами - как именно один слой передаёт данные другому и какие допущения при этом нарушаются. Хочется руки в крови - на HackerLab есть web-задачи с HTTP smuggling и обходом авторизации, где расхождение путей между слоями и есть ключ к флагу.