
PoC — межсайтовые запросы повторно используют настроенный API-ключ провайдера в inference-gateway (GHSA-5293-fcm6-fh8v, CVE-2026-87009, CVSS 5.4).
| Исследователь | Dostxodjayev Abdullox (@squeeze440) |
| Рекомендация | GHSA-5293-fcm6-fh8v |
| CVE | CVE-2026-87009 |
| CVSS 3.1 | 5.4 (Средний) — CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:L |
| Слабость | CWE-352, CWE-306, CWE-346 |
| Статус | Исправлено в v0.46.0 |
Краткое описание: inference-gateway привязывается к 0.0.0.0 и по умолчанию поставляется с отключённой аутентификацией (AUTH_ENABLED=false), а его сквозной маршрут ANY /proxy/:provider/*path безусловно удаляет любой предоставленный вызывающей стороной заголовок Authorization и заменяет его собственным API-ключом провайдера, настроенным на сервере оператора шлюза, перед перенаправлением вверх по потоку, без политики CORS и без какой-либо защиты от CSRF, что позволяет любой веб-странице, которую посещает браузер жертвы, незаметно инициировать оплачиваемые запросы к LLM через собственную учётную запись OpenAI/Anthropic/и т. д. жертвы.
Продукт: inference-gateway/inference-gateway — самостоятельно размещаемый облачно-ориентированный шлюз LLM (Go, Gin).
Протестированная версия: коммит 6677da6afd0a606899f833c2351635edeef386f5 (main, 2026-08-04). Затронуто: <= 0.45.0.
Три независимых факта складываются в эту уязвимость.
1. Аутентификация отключена, а привязка по умолчанию публичная.
config/config.go:77 — AuthConfig.Enabled по умолчанию имеет значение false. config/config.go:94 — ServerConfig.Host по умолчанию имеет значение 0.0.0.0. При отключённой аутентификации NewOIDCAuthenticatorMiddleware (api/middlewares/auth.go:27-30) возвращает OIDCAuthenticatorNoop, чей Middleware() (api/middlewares/auth.go:48-52) является чистым сквозным пропуском. В этом режиме нет проверки идентичности для каждого запроса ни на одном маршруте. Быстрый старт examples/docker-compose/basic/docker-compose.yml публикует 8080:8080 без установленного AUTH_ENABLED, поэтому документированный путь начала работы приводит именно к такой конфигурации.
2. /proxy/:provider/*path всегда внедряет собственный ключ провайдера оператора.
api/routes.go:102-131 (ProxyHandler) вызывает applyProviderAuth, api/routes.go:287-312:
func applyProviderAuth(req *http.Request, provider core.IProvider) error {
req.Header.Del("Authorization") // caller's own Authorization header is discarded
token := provider.GetToken() // the operator's configured key (env var, e.g. OPENAI_API_KEY)
switch provider.GetAuthType() {
case constants.AuthTypeBearer:
req.Header.Set("Authorization", "Bearer "+token)
...
Не существует пути в коде, где используется собственный учётный данные вызывающей стороны; дизайн всегда подставляет настроенный ключ шлюза. В сочетании с фактом 1 неаутентифицированный вызывающий получает реальный ключ оператора бесплатно.
3. Ни политики CORS, ни защиты от CSRF не существует нигде в цепочке middleware.
cmd/gateway/main.go:271 создаёт роутер с помощью gin.New() (без middleware по умолчанию); цепочка (:273-290) — это otel → logger → telemetry → OIDC auth → guardrails → MCP. go.mod/go.sum не содержат пакета CORS. Заголовок Access-Control-Allow-Origin никогда не отправляется. Обработчику прокси не нужен ни пользовательский заголовок, ни CORS-небезопасный Content-Type для работы (он пересылает необработанное тело, затем перезаписывает исходящий Content-Type на application/json в api/routes.go:254), поэтому «простой» кросс-доменный fetch() (Content-Type: text/plain, без пользовательских заголовков) отправляется браузером без preflight. Серверный оплачиваемый запрос завершается независимо от клиентского применения CORS браузером.
Итоговый эффект: любой источник, который посещает браузер жертвы, пока шлюз доступен из этого браузера (loopback, LAN или публично, если оператор следовал документированному шаблону публикации 8080:8080), может инициировать произвольные выбранные злоумышленником завершения чата через реальную учётную запись провайдера оператора, с нулевой аутентификацией и без какого-либо видимого пользователю признака.
Динамически подтверждено сквозным образом с помощью реального браузера Chrome, выполняющего подлинный кросс-доменный запрос между двумя различными loopback-источниками (шлюз на 127.0.0.1, страница злоумышленника на 127.0.0.2). См. poc/:
poc/attacker_site/attack.html — точная страница, отдаваемая с источника злоумышленника; её единственное действие при загрузке — один fetch() к /proxy/openai/chat/completions.poc/mock_upstream.py — заменяет api.openai.com, логирует заголовок Authorization, Origin и тело, которое получает.poc/README.md — полные шаги запуска.Наблюдалось: кросс-доменный запрос браузера (Origin: http://127.0.0.2:8000) достиг mock-сервера выше по потоку, неся Authorization: Bearer sk-proj-VICTIM-REAL-BILLED-KEY-... и выбранное злоумышленником тело {"messages":[{"role":"user","content":"CSRF-DRIVEBY-MARKER-8271"}]} — страница злоумышленника никогда не обладала, не видела и не запрашивала никаких учётных данных. Проверка сетевого трафика браузера подтвердила, что POST http://127.0.0.1:8081/proxy/openai/chat/completions [200] был инициирован со страницы 127.0.0.2:8000. Полные доказательства, управляемые браузером (сетевой лог, скриншот), приложены к GHSA-5293-fcm6-fh8v.
Любой оператор, запускающий inference-gateway с его документированной конфигурацией по умолчанию, позволяет использовать свои настроенные API-ключи провайдера любой веб-странице, доступной браузеру, который может достичь порта шлюза, без учётных данных, cookie или особого сетевого положения, кроме «может отправить HTTP-запрос на адрес шлюза». Конкретно: несанкционированное потребление биллинга/квоты на собственной учётной записи провайдера оператора, слепо инициируемое любым сторонним веб-сайтом, рекламой или скомпрометированной страницей, открытой у оператора (или у любого в той же LAN), пока шлюз работает. Браузер блокирует злоумышленнику чтение вывода модели (нет заголовков CORS), поэтому это слепая принудительная транзакция, а не примитив чтения.
Origin/Sec-Fetch-Site и без ограничения CORS./health, имеет нулевую проверку идентичности для каждого запроса, когда AUTH_ENABLED=false, что является документированным значением по умолчанию.Исправлено в v0.46.0 (сопровождающий ужесточил значения по умолчанию). Рекомендуемые меры:
/proxy/:provider/*path (и других изменяющих состояние маршрутах), что вынуждает CORS preflight для кросс-доменных вызывающих и даёт шлюзу место для применения списка разрешённых источников. Это закрывает обход через «простой запрос» без необходимости включать аутентификацию.SERVER_HOST с 0.0.0.0 на 127.0.0.1, требуя явного согласия на более широкий интерфейс (как сделал Ollama для того же класса уязвимостей).AUTH_ENABLED=false и SERVER_HOST не является loopback.README.md / Configurations.md.Dostxodjayev Abdullox (@squeeze440)