
PoC — le richieste cross-origin riutilizzano la chiave API del provider configurata in inference-gateway (GHSA-5293-fcm6-fh8v, CVE-2026-87009, CVSS 5.4).
| Ricercatore | Dostxodjayev Abdullox (@squeeze440) |
| Avviso | GHSA-5293-fcm6-fh8v |
| CVE | CVE-2026-87009 |
| CVSS 3.1 | 5.4 (Medio) — CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:L |
| Debolezza | CWE-352, CWE-306, CWE-346 |
| Stato | Risolto in v0.46.0 |
Riepilogo: inference-gateway si associa a 0.0.0.0 e distribuisce l'autenticazione disabilitata (AUTH_ENABLED=false) per impostazione predefinita, e la sua rotta passthrough ANY /proxy/:provider/*path rimuove incondizionatamente qualsiasi header Authorization fornito dal chiamante e lo sostituisce con la chiave API del provider configurata sul server dall'operatore del gateway prima di inoltrarla a monte, senza alcuna policy CORS e senza alcuna protezione CSRF di alcun tipo, consentendo a qualsiasi pagina web visitata dal browser della vittima di condurre silenziosamente richieste LLM fatturate attraverso l'account OpenAI/Anthropic/ecc. della vittima stessa.
Prodotto: inference-gateway/inference-gateway — gateway LLM self-hosted, cloud-native (Go, Gin).
Versione testata: commit 6677da6afd0a606899f833c2351635edeef386f5 (main, 2026-08-04). Affette: <= 0.45.0.
Tre fatti indipendenti si combinano nel bug.
1. L'autenticazione è disattivata e il bind è pubblico per impostazione predefinita.
config/config.go:77 — AuthConfig.Enabled è impostato per impostazione predefinita a false. config/config.go:94 — ServerConfig.Host è impostato per impostazione predefinita a 0.0.0.0. Con l'autenticazione disabilitata, NewOIDCAuthenticatorMiddleware (api/middlewares/auth.go:27-30) restituisce OIDCAuthenticatorNoop, il cui Middleware() (api/middlewares/auth.go:48-52) è un puro passthrough. Non esiste alcun controllo di identità per-richiesta su nessuna rotta in questa modalità. Il quickstart examples/docker-compose/basic/docker-compose.yml pubblica 8080:8080 senza AUTH_ENABLED impostato, quindi il percorso documentato di avvio produce esattamente questa configurazione.
2. /proxy/:provider/*path inietta sempre la chiave del provider dell'operatore stesso.
api/routes.go:102-131 (ProxyHandler) chiama 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)
...
Non esiste alcun percorso di codice in cui venga utilizzata la credenziale del chiamante stesso; il design sostituisce sempre la chiave configurata del gateway. Combinato con il fatto 1, un chiamante non autenticato ottiene gratuitamente la chiave reale dell'operatore allegata.
3. Non esiste alcuna policy CORS e alcuna protezione CSRF in nessun punto della catena di middleware.
cmd/gateway/main.go:271 costruisce il router con gin.New() (nessun middleware predefinito); la catena (:273-290) è otel → logger → telemetry → OIDC auth → guardrails → MCP. go.mod/go.sum non contengono alcun pacchetto CORS. Nessun header Access-Control-Allow-Origin viene mai inviato. Il proxy handler non necessita di alcun header personalizzato o Content-Type non-CORS-safe per funzionare (inoltra il corpo grezzo, poi sovrascrive il Content-Type in uscita a application/json in api/routes.go:254), quindi un fetch() cross-origin "semplice" (Content-Type: text/plain, nessun header personalizzato) viene inviato dal browser senza preflight. La richiesta fatturata lato server si completa indipendentemente dall'applicazione CORS lato lettura del browser.
Effetto netto: qualsiasi origine visitata dal browser di una vittima, mentre il gateway è raggiungibile da quel browser (loopback, LAN o pubblico se l'operatore ha seguito il modello documentato di pubblicazione 8080:8080), può condurre chat completion arbitrarie scelte dall'attaccante attraverso l'account reale del provider dell'operatore, con autenticazione zero e nessuna indicazione visibile all'utente.
Confermato dinamicamente end-to-end con un browser Chrome reale che effettua una richiesta cross-origin genuina tra due origini loopback distinte (gateway su 127.0.0.1, pagina dell'attaccante su 127.0.0.2). Vedi poc/:
poc/attacker_site/attack.html — la pagina esatta servita dall'origine dell'attaccante; la sua unica azione al caricamento è un fetch() verso /proxy/openai/chat/completions.poc/mock_upstream.py — sostituisce api.openai.com, registra l'header Authorization, l'Origin e il corpo che riceve.poc/README.md — passaggi completi di esecuzione.Osservato: la richiesta cross-origin del browser (Origin: http://127.0.0.2:8000) ha raggiunto il mock upstream trasportando Authorization: Bearer sk-proj-VICTIM-REAL-BILLED-KEY-... e il corpo scelto dall'attaccante {"messages":[{"role":"user","content":"CSRF-DRIVEBY-MARKER-8271"}]} — la pagina dell'attaccante non ha mai posseduto, visto o richiesto alcuna credenziale. L'ispezione di rete del browser ha confermato che POST http://127.0.0.1:8081/proxy/openai/chat/completions [200] è stato inviato dalla pagina 127.0.0.2:8000. Prove complete guidate dal browser (log di rete, screenshot) sono allegate a GHSA-5293-fcm6-fh8v.
Qualsiasi operatore che esegue inference-gateway con la sua configurazione predefinita documentata ha le proprie chiavi API del provider configurate utilizzabili da qualsiasi pagina web raggiungibile da un browser che può raggiungere la porta del gateway, senza credenziali, cookie o posizione di rete speciale oltre a "può inviare una richiesta HTTP all'indirizzo del gateway". Concretamente: consumo non autorizzato di fatturazione/quota sull'account del provider dell'operatore stesso, guidato ciecamente da qualsiasi sito web di terze parti, annuncio o pagina compromessa che l'operatore (o chiunque sulla stessa LAN) ha aperto mentre il gateway è in esecuzione. Il browser blocca l'attaccante dalla lettura dell'output del modello (nessun header CORS), quindi questa è una transazione forzata cieca, non una primitiva di lettura.
Origin/Sec-Fetch-Site e senza restrizione CORS./health ha zero controlli di identità per-richiesta quando AUTH_ENABLED=false, l'impostazione predefinita documentata.Risolto in v0.46.0 (il maintainer ha rafforzato le impostazioni predefinite). Misure raccomandate:
/proxy/:provider/*path (e altre rotte che modificano lo stato), il che forza un preflight CORS per i chiamanti cross-origin e dà al gateway un punto in cui applicare un allow-list di origini. Questo chiude il bypass della "richiesta semplice" senza richiedere che l'autenticazione sia abilitata.SERVER_HOST da 0.0.0.0 a 127.0.0.1, richiedendo un opt-in esplicito per un'interfaccia più ampia (come ha fatto Ollama per la stessa classe di bug).AUTH_ENABLED=false e SERVER_HOST non è loopback.README.md / Configurations.md.Dostxodjayev Abdullox (@squeeze440)