
PoC — cross-origin requests reuse the configured provider API key in inference-gateway (GHSA-5293-fcm6-fh8v, CVE-2026-87009, CVSS 5.4).
| Researcher | Dostxodjayev Abdullox (@squeeze440) |
| Advisory | GHSA-5293-fcm6-fh8v |
| CVE | CVE-2026-87009 |
| CVSS 3.1 | 5.4 (Medium) — CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:L |
| Weakness | CWE-352, CWE-306, CWE-346 |
| Status | Fixed in v0.46.0 |
Summary: inference-gateway binds to 0.0.0.0 and ships authentication disabled (AUTH_ENABLED=false) by default, and its ANY /proxy/:provider/*path passthrough route unconditionally strips any caller-supplied Authorization header and replaces it with the gateway operator's own server-configured provider API key before forwarding upstream, with no CORS policy and no CSRF protection of any kind, allowing any web page a victim's browser visits to silently drive billed LLM requests through the victim's own OpenAI/Anthropic/etc. account.
Product: inference-gateway/inference-gateway — self-hosted, cloud-native LLM gateway (Go, Gin).
Tested version: commit 6677da6afd0a606899f833c2351635edeef386f5 (main, 2026-08-04). Affected: <= 0.45.0.
Three independent facts combine into the bug.
1. Auth is off and the bind is public by default.
config/config.go:77 — AuthConfig.Enabled defaults to false. config/config.go:94 — ServerConfig.Host defaults to 0.0.0.0. With auth disabled, NewOIDCAuthenticatorMiddleware (api/middlewares/auth.go:27-30) returns OIDCAuthenticatorNoop, whose Middleware() (api/middlewares/auth.go:48-52) is a pure passthrough. There is no per-request identity check on any route in this mode. The quickstart examples/docker-compose/basic/docker-compose.yml publishes 8080:8080 with no AUTH_ENABLED set, so the documented getting-started path produces exactly this configuration.
2. /proxy/:provider/*path always injects the operator's own provider key.
api/routes.go:102-131 (ProxyHandler) calls 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)
...
There is no code path where the caller's own credential is used; the design always substitutes the gateway's configured key. Combined with fact 1, an unauthenticated caller gets the operator's real key attached for free.
3. No CORS policy and no CSRF protection exist anywhere in the middleware chain.
cmd/gateway/main.go:271 builds the router with gin.New() (no default middleware); the chain (:273-290) is otel → logger → telemetry → OIDC auth → guardrails → MCP. go.mod/go.sum contain no CORS package. No Access-Control-Allow-Origin header is ever sent. The proxy handler needs no custom header or CORS-unsafe Content-Type to work (it forwards the raw body, then overwrites the outbound Content-Type to application/json at api/routes.go:254), so a "simple" cross-origin fetch() (Content-Type: text/plain, no custom headers) is sent by the browser with no preflight. The server-side billed request completes independent of the browser's read-side CORS enforcement.
Net effect: any origin a victim's browser visits, while the gateway is reachable from that browser (loopback, LAN, or public if the operator followed the documented 8080:8080 publish pattern), can drive arbitrary attacker-chosen chat completions through the operator's real provider account, with zero authentication and no user-visible indication.
Dynamically confirmed end-to-end with a real Chrome browser making a genuine cross-origin request between two distinct loopback origins (gateway on 127.0.0.1, attacker page on 127.0.0.2). See poc/:
poc/attacker_site/attack.html — the exact page served from the attacker origin; its only action on load is one fetch() to /proxy/openai/chat/completions.poc/mock_upstream.py — stands in for api.openai.com, logs the Authorization header, Origin, and body it receives.poc/README.md — full run steps.Observed: the cross-origin browser request (Origin: http://127.0.0.2:8000) reached the mock upstream carrying Authorization: Bearer sk-proj-VICTIM-REAL-BILLED-KEY-... and the attacker-chosen body {"messages":[{"role":"user","content":"CSRF-DRIVEBY-MARKER-8271"}]} — the attacker page never possessing, seeing, or being asked for any credential. Browser network inspection confirmed POST http://127.0.0.1:8081/proxy/openai/chat/completions [200] fired from the 127.0.0.2:8000 page. Full browser-driven evidence (network log, screenshot) is attached to GHSA-5293-fcm6-fh8v.
Any operator running inference-gateway with its documented default configuration has their configured provider API key(s) usable by any web page reachable to a browser that can reach the gateway's port, with no credential, cookie, or special network position beyond "can send an HTTP request to the gateway's address". Concretely: unauthorized billing/quota consumption on the operator's own provider account, driven blindly by any third-party website, ad, or compromised page the operator (or anyone on the same LAN) has open while the gateway is running. The browser blocks the attacker from reading model output (no CORS headers), so this is a blind forced transaction, not a read primitive.
Origin/Sec-Fetch-Site check, and no CORS restriction./health has zero per-request identity check when AUTH_ENABLED=false, the documented default.Fixed in v0.46.0 (maintainer hardened the defaults). Recommended measures:
/proxy/:provider/*path (and other state-changing routes), which forces a CORS preflight for cross-origin callers and gives the gateway a place to enforce an origin allow-list. This closes the "simple request" bypass without requiring auth to be enabled.SERVER_HOST's default from 0.0.0.0 to 127.0.0.1, requiring explicit opt-in to a broader interface (as Ollama did for the same class of bug).AUTH_ENABLED=false and SERVER_HOST is not loopback.README.md / Configurations.md.Dostxodjayev Abdullox (@squeeze440)