
PoC — requisições cross-origin reutilizam a chave de API do provedor configurada no inference-gateway (GHSA-5293-fcm6-fh8v, CVE-2026-87009, CVSS 5.4).
| Pesquisador | Dostxodjayev Abdullox (@squeeze440) |
| Aviso | GHSA-5293-fcm6-fh8v |
| CVE | CVE-2026-87009 |
| CVSS 3.1 | 5.4 (Médio) — CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:L |
| Fraqueza | CWE-352, CWE-306, CWE-346 |
| Status | Corrigido em v0.46.0 |
Resumo: O inference-gateway vincula-se a 0.0.0.0 e é distribuído com a autenticação desativada (AUTH_ENABLED=false) por padrão, e a sua rota de passagem ANY /proxy/:provider/*path remove incondicionalmente qualquer cabeçalho Authorization fornecido pelo chamador e substitui-o pela própria chave de API do provedor configurada no servidor pelo operador do gateway antes de encaminhar para o upstream, sem qualquer política de CORS e sem qualquer proteção CSRF de qualquer tipo, permitindo que qualquer página web que o navegador da vítima visite acione silenciosamente pedidos de LLM faturados através da própria conta OpenAI/Anthropic/etc. da vítima.
Produto: inference-gateway/inference-gateway — gateway LLM auto-hospedado, cloud-native (Go, Gin).
Versão testada: commit 6677da6afd0a606899f833c2351635edeef386f5 (main, 2026-08-04). Afetadas: <= 0.45.0.
Três factos independentes combinam-se no bug.
1. A autenticação está desativada e o bind é público por padrão.
config/config.go:77 — AuthConfig.Enabled tem como padrão false. config/config.go:94 — ServerConfig.Host tem como padrão 0.0.0.0. Com a autenticação desativada, NewOIDCAuthenticatorMiddleware (api/middlewares/auth.go:27-30) devolve OIDCAuthenticatorNoop, cujo Middleware() (api/middlewares/auth.go:48-52) é um mero passthrough. Não existe verificação de identidade por pedido em nenhuma rota neste modo. O quickstart examples/docker-compose/basic/docker-compose.yml publica 8080:8080 sem AUTH_ENABLED definido, pelo que o caminho de iniciação documentado produz exatamente esta configuração.
2. /proxy/:provider/*path injeta sempre a própria chave do provedor do operador.
api/routes.go:102-131 (ProxyHandler) chama 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)
...
Não existe qualquer caminho de código onde a própria credencial do chamador seja utilizada; o design substitui sempre a chave configurada do gateway. Combinado com o facto 1, um chamador não autenticado obtém a chave real do operador anexada gratuitamente.
3. Não existe qualquer política de CORS nem proteção CSRF em nenhum ponto da cadeia de middleware.
cmd/gateway/main.go:271 constrói o router com gin.New() (sem middleware predefinido); a cadeia (:273-290) é otel → logger → telemetry → OIDC auth → guardrails → MCP. go.mod/go.sum não contêm qualquer pacote de CORS. Nunca é enviado qualquer cabeçalho Access-Control-Allow-Origin. O handler de proxy não necessita de qualquer cabeçalho personalizado nem de um Content-Type não seguro para CORS para funcionar (encaminha o corpo em bruto e depois substitui o Content-Type de saída por application/json em api/routes.go:254), pelo que um fetch() cross-origin "simples" (Content-Type: text/plain, sem cabeçalhos personalizados) é enviado pelo navegador sem preflight. O pedido faturado do lado do servidor é concluído independentemente da aplicação de CORS do lado da leitura do navegador.
Efeito líquido: qualquer origem que o navegador da vítima visite, enquanto o gateway for acessível a partir desse navegador (loopback, LAN ou público se o operador seguiu o padrão documentado de publicação 8080:8080), pode acionar completações de chat arbitrárias escolhidas pelo atacante através da conta real do provedor do operador, com zero autenticação e sem qualquer indicação visível ao utilizador.
Confirmado dinamicamente de ponta a ponta com um navegador Chrome real a efetuar um pedido cross-origin genuíno entre duas origens de loopback distintas (gateway em 127.0.0.1, página do atacante em 127.0.0.2). Ver poc/:
poc/attacker_site/attack.html — a página exata servida a partir da origem do atacante; a sua única ação ao carregar é um fetch() para /proxy/openai/chat/completions.poc/mock_upstream.py — substitui api.openai.com, registando o cabeçalho Authorization, o Origin e o corpo que recebe.poc/README.md — passos completos de execução.Observado: o pedido cross-origin do navegador (Origin: http://127.0.0.2:8000) chegou ao mock upstream transportando Authorization: Bearer sk-proj-VICTIM-REAL-BILLED-KEY-... e o corpo escolhido pelo atacante {"messages":[{"role":"user","content":"CSRF-DRIVEBY-MARKER-8271"}]} — a página do atacante nunca possuiu, viu ou lhe foi pedida qualquer credencial. A inspeção de rede do navegador confirmou que POST http://127.0.0.1:8081/proxy/openai/chat/completions [200] foi disparado a partir da página 127.0.0.2:8000. As evidências completas conduzidas pelo navegador (registo de rede, captura de ecrã) estão anexadas a GHSA-5293-fcm6-fh8v.
Qualquer operador que execute o inference-gateway com a sua configuração predefinida documentada tem a(s) sua(s) chave(s) de API do provedor configurada(s) utilizável(is) por qualquer página web acessível a um navegador que consiga alcançar a porta do gateway, sem qualquer credencial, cookie ou posição de rede especial além de "conseguir enviar um pedido HTTP para o endereço do gateway". Concretamente: consumo não autorizado de faturação/quota na própria conta do provedor do operador, acionado cegamente por qualquer site de terceiros, anúncio ou página comprometida que o operador (ou qualquer pessoa na mesma LAN) tenha aberto enquanto o gateway está em execução. O navegador impede o atacante de ler a saída do modelo (sem cabeçalhos CORS), pelo que se trata de uma transação forçada cega, não de uma primitiva de leitura.
Origin/Sec-Fetch-Site e sem restrição de CORS./health têm zero verificação de identidade por pedido quando AUTH_ENABLED=false, o padrão documentado.Corrigido em v0.46.0 (o mantenedor reforçou os padrões). Medidas recomendadas:
/proxy/:provider/*path (e noutras rotas que alteram o estado), o que força um preflight CORS para chamadores cross-origin e dá ao gateway um local para aplicar uma lista de origens permitidas. Isto fecha o bypass de "pedido simples" sem exigir que a autenticação esteja ativada.SERVER_HOST de 0.0.0.0 para 127.0.0.1, exigindo opt-in explícito para uma interface mais ampla (como a Ollama fez para a mesma classe de bug).AUTH_ENABLED=false e SERVER_HOST não for loopback.README.md / Configurations.md.Dostxodjayev Abdullox (@squeeze440)