
PoC — Cross-Origin-Anfragen verwenden den konfigurierten Provider-API-Schlüssel in inference-gateway wieder (GHSA-5293-fcm6-fh8v, CVE-2026-87009, CVSS 5.4).
| Forscher | Dostxodjayev Abdullox (@squeeze440) |
| Advisory | GHSA-5293-fcm6-fh8v |
| CVE | CVE-2026-87009 |
| CVSS 3.1 | 5.4 (Mittel) — CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:L |
| Schwachstelle | CWE-352, CWE-306, CWE-346 |
| Status | Behoben in v0.46.0 |
Zusammenfassung: inference-gateway bindet an 0.0.0.0 und liefert die Authentifizierung standardmäßig deaktiviert aus (AUTH_ENABLED=false), und seine Passthrough-Route ANY /proxy/:provider/*path entfernt bedingungslos jeden vom Aufrufer mitgelieferten Authorization-Header und ersetzt ihn durch den eigenen, serverseitig konfigurierten Provider-API-Schlüssel des Gateway-Betreibers, bevor die Anfrage weitergeleitet wird – ohne CORS-Richtlinie und ohne jeglichen CSRF-Schutz. Dadurch kann jede Webseite, die der Browser eines Opfers besucht, stillschweigend abgerechnete LLM-Anfragen über das eigene OpenAI/Anthropic/etc.-Konto des Opfers auslösen.
Produkt: inference-gateway/inference-gateway — selbstgehostetes, cloud-natives LLM-Gateway (Go, Gin).
Getestete Version: Commit 6677da6afd0a606899f833c2351635edeef386f5 (main, 2026-08-04). Betroffen: <= 0.45.0.
Drei unabhängige Fakten ergeben zusammen den Fehler.
1. Auth ist aus und die Bindung ist standardmäßig öffentlich.
config/config.go:77 — AuthConfig.Enabled ist standardmäßig false. config/config.go:94 — ServerConfig.Host ist standardmäßig 0.0.0.0. Bei deaktivierter Auth gibt NewOIDCAuthenticatorMiddleware (api/middlewares/auth.go:27-30) OIDCAuthenticatorNoop zurück, dessen Middleware() (api/middlewares/auth.go:48-52) ein reiner Passthrough ist. In diesem Modus gibt es auf keiner Route eine Identitätsprüfung pro Anfrage. Die Quickstart-Datei examples/docker-compose/basic/docker-compose.yml veröffentlicht 8080:8080 ohne gesetztes AUTH_ENABLED, sodass der dokumentierte Einstiegspfad genau diese Konfiguration erzeugt.
2. /proxy/:provider/*path injiziert immer den eigenen Provider-Schlüssel des Betreibers.
api/routes.go:102-131 (ProxyHandler) ruft applyProviderAuth auf, 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)
...
Es gibt keinen Codepfad, in dem die eigene Berechtigung des Aufrufers verwendet wird; das Design ersetzt stets den konfigurierten Schlüssel des Gateways. In Kombination mit Fakt 1 erhält ein nicht authentifizierter Aufrufer den echten Schlüssel des Betreibers kostenlos angehängt.
3. Es existiert nirgends in der Middleware-Kette eine CORS-Richtlinie oder ein CSRF-Schutz.
cmd/gateway/main.go:271 erstellt den Router mit gin.New() (keine Standard-Middleware); die Kette (:273-290) ist otel → logger → telemetry → OIDC auth → guardrails → MCP. go.mod/go.sum enthalten kein CORS-Paket. Es wird nie ein Access-Control-Allow-Origin-Header gesendet. Der Proxy-Handler benötigt weder einen benutzerdefinierten Header noch einen CORS-unsicheren Content-Type, um zu funktionieren (er leitet den rohen Body weiter und überschreibt dann den ausgehenden Content-Type in api/routes.go:254 mit application/json), sodass ein „einfacher" Cross-Origin-fetch() (Content-Type: text/plain, keine benutzerdefinierten Header) vom Browser ohne Preflight gesendet wird. Die serverseitig abgerechnete Anfrage wird unabhängig von der leseseitigen CORS-Durchsetzung des Browsers abgeschlossen.
Nettoeffekt: Jede Origin, die der Browser eines Opfers besucht, während das Gateway von diesem Browser aus erreichbar ist (Loopback, LAN oder öffentlich, wenn der Betreiber dem dokumentierten 8080:8080-Veröffentlichungsmuster gefolgt ist), kann beliebige, vom Angreifer gewählte Chat-Completions über das echte Provider-Konto des Betreibers auslösen – ohne jegliche Authentifizierung und ohne für den Benutzer sichtbaren Hinweis.
Dynamisch Ende-zu-Ende bestätigt mit einem echten Chrome-Browser, der eine echte Cross-Origin-Anfrage zwischen zwei verschiedenen Loopback-Origins durchführt (Gateway auf 127.0.0.1, Angreiferseite auf 127.0.0.2). Siehe poc/:
poc/attacker_site/attack.html — die exakte Seite, die von der Angreifer-Origin ausgeliefert wird; ihre einzige Aktion beim Laden ist ein fetch() an /proxy/openai/chat/completions.poc/mock_upstream.py — steht stellvertretend für api.openai.com und protokolliert den Authorization-Header, Origin und den empfangenen Body.poc/README.md — vollständige Ausführungsschritte.Beobachtet: Die Cross-Origin-Browser-Anfrage (Origin: http://127.0.0.2:8000) erreichte das Mock-Upstream mit Authorization: Bearer sk-proj-VICTIM-REAL-BILLED-KEY-... und dem vom Angreifer gewählten Body {"messages":[{"role":"user","content":"CSRF-DRIVEBY-MARKER-8271"}]} — wobei die Angreiferseite niemals eine Berechtigung besaß, sah oder danach gefragt wurde. Die Netzwerkanalyse des Browsers bestätigte, dass POST http://127.0.0.1:8081/proxy/openai/chat/completions [200] von der Seite 127.0.0.2:8000 ausgelöst wurde. Vollständige browserbasierte Nachweise (Netzwerkprotokoll, Screenshot) sind an GHSA-5293-fcm6-fh8v angehängt.
Jeder Betreiber, der inference-gateway mit seiner dokumentierten Standardkonfiguration ausführt, ermöglicht es jeder Webseite, die für einen Browser erreichbar ist, der den Port des Gateways erreichen kann, die konfigurierten Provider-API-Schlüssel zu nutzen – ohne Berechtigung, Cookie oder besondere Netzwerkposition, die über „kann eine HTTP-Anfrage an die Adresse des Gateways senden" hinausgeht. Konkret: unbefugter Verbrauch von Abrechnung/Kontingent auf dem eigenen Provider-Konto des Betreibers, blind ausgelöst durch jede Website eines Drittanbieters, jede Werbung oder jede kompromittierte Seite, die der Betreiber (oder jeder im selben LAN) geöffnet hat, während das Gateway läuft. Der Browser blockiert den Angreifer daran, die Modellausgabe zu lesen (keine CORS-Header), sodass dies eine blinde erzwungene Transaktion ist, kein Leseprimitiv.
Origin/Sec-Fetch-Site-Prüfung und ohne CORS-Beschränkung ausgeführt wird./health hat keine Identitätsprüfung pro Anfrage, wenn AUTH_ENABLED=false gilt, der dokumentierte Standard.Behoben in v0.46.0 (der Maintainer hat die Standardeinstellungen gehärtet). Empfohlene Maßnahmen:
/proxy/:provider/*path (und anderen zustandsändernden Routen) verlangen, was für Cross-Origin-Aufrufer einen CORS-Preflight erzwingt und dem Gateway eine Stelle gibt, an der eine Origin-Allow-List durchgesetzt werden kann. Dies schließt die „Simple-Request"-Umgehung, ohne dass Auth aktiviert werden muss.SERVER_HOST von 0.0.0.0 auf 127.0.0.1 ändern, sodass ein explizites Opt-in für eine breitere Schnittstelle erforderlich ist (wie Ollama es für dieselbe Fehlerklasse getan hat).AUTH_ENABLED=false gilt und SERVER_HOST nicht Loopback ist.README.md / Configurations.md dokumentieren.Dostxodjayev Abdullox (@squeeze440)