
PoC — las solicitudes de origen cruzado reutilizan la clave API del proveedor configurada en inference-gateway (GHSA-5293-fcm6-fh8v, CVE-2026-87009, CVSS 5.4).
| Investigador | Dostxodjayev Abdullox (@squeeze440) |
| Aviso | 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 |
| Debilidad | CWE-352, CWE-306, CWE-346 |
| Estado | Corregido en v0.46.0 |
Resumen: inference-gateway se enlaza a 0.0.0.0 y distribuye la autenticación deshabilitada (AUTH_ENABLED=false) por defecto, y su ruta de paso directo ANY /proxy/:provider/*path elimina incondicionalmente cualquier cabecera Authorization proporcionada por el llamador y la reemplaza con la propia clave de API del proveedor configurada en el servidor por el operador del gateway antes de reenviarla al upstream, sin política CORS y sin protección CSRF de ningún tipo, lo que permite que cualquier página web que visite el navegador de una víctima impulse silenciosamente solicitudes facturadas a LLM a través de la propia cuenta de OpenAI/Anthropic/etc. de la víctima.
Producto: inference-gateway/inference-gateway — gateway LLM autoalojado y nativo de la nube (Go, Gin).
Versión probada: commit 6677da6afd0a606899f833c2351635edeef386f5 (main, 2026-08-04). Afectadas: <= 0.45.0.
Tres hechos independientes se combinan en el fallo.
1. La autenticación está desactivada y el enlace es público por defecto.
config/config.go:77 — AuthConfig.Enabled es false por defecto. config/config.go:94 — ServerConfig.Host es 0.0.0.0 por defecto. Con la autenticación deshabilitada, NewOIDCAuthenticatorMiddleware (api/middlewares/auth.go:27-30) devuelve OIDCAuthenticatorNoop, cuyo Middleware() (api/middlewares/auth.go:48-52) es un paso directo puro. No hay ninguna comprobación de identidad por solicitud en ninguna ruta en este modo. El quickstart examples/docker-compose/basic/docker-compose.yml publica 8080:8080 sin establecer AUTH_ENABLED, por lo que la ruta de inicio documentada produce exactamente esta configuración.
2. /proxy/:provider/*path siempre inyecta la propia clave del proveedor del operador.
api/routes.go:102-131 (ProxyHandler) llama a 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)
...
No existe ninguna ruta de código en la que se utilice la propia credencial del llamador; el diseño siempre sustituye la clave configurada del gateway. Combinado con el hecho 1, un llamador no autenticado obtiene la clave real del operador adjunta de forma gratuita.
3. No existe ninguna política CORS ni protección CSRF en ningún lugar de la cadena de middleware.
cmd/gateway/main.go:271 construye el router con gin.New() (sin middleware por defecto); la cadena (:273-290) es otel → logger → telemetry → OIDC auth → guardrails → MCP. go.mod/go.sum no contienen ningún paquete CORS. Nunca se envía la cabecera Access-Control-Allow-Origin. El manejador del proxy no necesita ninguna cabecera personalizada ni un Content-Type no seguro para CORS para funcionar (reenvía el cuerpo sin procesar y luego sobrescribe el Content-Type de salida a application/json en api/routes.go:254), por lo que un fetch() cross-origin "simple" (Content-Type: text/plain, sin cabeceras personalizadas) es enviado por el navegador sin preflight. La solicitud facturada del lado del servidor se completa independientemente de la aplicación de CORS del lado de lectura del navegador.
Efecto neto: cualquier origen que visite el navegador de una víctima, mientras el gateway sea accesible desde ese navegador (loopback, LAN o público si el operador siguió el patrón de publicación documentado 8080:8080), puede impulsar finalizaciones de chat arbitrarias elegidas por el atacante a través de la cuenta real del proveedor del operador, con cero autenticación y sin ninguna indicación visible para el usuario.
Confirmado dinámicamente de extremo a extremo con un navegador Chrome real realizando una solicitud cross-origin genuina entre dos orígenes de loopback distintos (gateway en 127.0.0.1, página del atacante en 127.0.0.2). Véase poc/:
poc/attacker_site/attack.html — la página exacta servida desde el origen del atacante; su única acción al cargar es un fetch() a /proxy/openai/chat/completions.poc/mock_upstream.py — sustituye a api.openai.com, registra la cabecera Authorization, el Origin y el cuerpo que recibe.poc/README.md — pasos completos de ejecución.Observado: la solicitud cross-origin del navegador (Origin: http://127.0.0.2:8000) llegó al upstream simulado portando Authorization: Bearer sk-proj-VICTIM-REAL-BILLED-KEY-... y el cuerpo elegido por el atacante {"messages":[{"role":"user","content":"CSRF-DRIVEBY-MARKER-8271"}]} — la página del atacante nunca poseyó, vio ni le fue solicitada ninguna credencial. La inspección de red del navegador confirmó que POST http://127.0.0.1:8081/proxy/openai/chat/completions [200] se disparó desde la página 127.0.0.2:8000. La evidencia completa impulsada por el navegador (registro de red, captura de pantalla) está adjunta a GHSA-5293-fcm6-fh8v.
Cualquier operador que ejecute inference-gateway con su configuración predeterminada documentada tiene su(s) clave(s) de API del proveedor configurada(s) utilizables por cualquier página web accesible a un navegador que pueda alcanzar el puerto del gateway, sin credencial, cookie ni posición de red especial más allá de "poder enviar una solicitud HTTP a la dirección del gateway". Concretamente: consumo no autorizado de facturación/cuota en la propia cuenta del proveedor del operador, impulsado ciegamente por cualquier sitio web de terceros, anuncio o página comprometida que el operador (o cualquier persona en la misma LAN) tenga abierta mientras el gateway esté en ejecución. El navegador bloquea que el atacante lea la salida del modelo (sin cabeceras CORS), por lo que se trata de una transacción forzada ciega, no de una primitiva de lectura.
Origin/Sec-Fetch-Site y sin restricción CORS./health tienen cero comprobación de identidad por solicitud cuando AUTH_ENABLED=false, el valor predeterminado documentado.Corregido en v0.46.0 (el mantenedor reforzó los valores predeterminados). Medidas recomendadas:
/proxy/:provider/*path (y otras rutas que cambian el estado), lo que fuerza un preflight CORS para los llamadores cross-origin y da al gateway un lugar donde aplicar una lista de orígenes permitidos. Esto cierra el bypass de "solicitud simple" sin requerir que la autenticación esté habilitada.SERVER_HOST de 0.0.0.0 a 127.0.0.1, requiriendo una adhesión explícita a una interfaz más amplia (como hizo Ollama para la misma clase de fallo).AUTH_ENABLED=false y SERVER_HOST no sea loopback.README.md / Configurations.md.Dostxodjayev Abdullox (@squeeze440)