
PoC — les requêtes cross-origin réutilisent la clé API du fournisseur configurée dans inference-gateway (GHSA-5293-fcm6-fh8v, CVE-2026-87009, CVSS 5.4).
| Chercheur | Dostxodjayev Abdullox (@squeeze440) |
| Avis | GHSA-5293-fcm6-fh8v |
| CVE | CVE-2026-87009 |
| CVSS 3.1 | 5.4 (Moyen) — CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:L |
| Faiblesse | CWE-352, CWE-306, CWE-346 |
| Statut | Corrigé dans v0.46.0 |
Résumé : inference-gateway s'attache à 0.0.0.0 et est livré avec l'authentification désactivée (AUTH_ENABLED=false) par défaut, et sa route de passthrough ANY /proxy/:provider/*path supprime inconditionnellement tout en-tête Authorization fourni par l'appelant et le remplace par la propre clé API du fournisseur configurée côté serveur par l'opérateur de la passerelle avant de transmettre en amont, sans aucune politique CORS et sans aucune protection CSRF, permettant à n'importe quelle page web visitée par le navigateur d'une victime de déclencher silencieusement des requêtes LLM facturées via le propre compte OpenAI/Anthropic/etc. de la victime.
Produit : inference-gateway/inference-gateway — passerelle LLM auto-hébergée, cloud-native (Go, Gin).
Version testée : commit 6677da6afd0a606899f833c2351635edeef386f5 (main, 2026-08-04). Affecté : <= 0.45.0.
Trois faits indépendants se combinent pour former la vulnérabilité.
1. L'authentification est désactivée et l'attachement est public par défaut.
config/config.go:77 — AuthConfig.Enabled vaut false par défaut. config/config.go:94 — ServerConfig.Host vaut 0.0.0.0 par défaut. Avec l'authentification désactivée, NewOIDCAuthenticatorMiddleware (api/middlewares/auth.go:27-30) renvoie OIDCAuthenticatorNoop, dont Middleware() (api/middlewares/auth.go:48-52) est un pur passthrough. Il n'y a aucune vérification d'identité par requête sur aucune route dans ce mode. Le quickstart examples/docker-compose/basic/docker-compose.yml publie 8080:8080 sans AUTH_ENABLED défini, donc le parcours de démarrage documenté produit exactement cette configuration.
2. /proxy/:provider/*path injecte toujours la propre clé du fournisseur de l'opérateur.
api/routes.go:102-131 (ProxyHandler) appelle 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)
...
Il n'existe aucun chemin de code où les propres identifiants de l'appelant sont utilisés ; la conception substitue toujours la clé configurée de la passerelle. Combiné au fait 1, un appelant non authentifié obtient gratuitement la vraie clé de l'opérateur attachée.
3. Aucune politique CORS et aucune protection CSRF n'existent nulle part dans la chaîne de middlewares.
cmd/gateway/main.go:271 construit le routeur avec gin.New() (aucun middleware par défaut) ; la chaîne (:273-290) est otel → logger → telemetry → OIDC auth → guardrails → MCP. go.mod/go.sum ne contiennent aucun paquet CORS. Aucun en-tête Access-Control-Allow-Origin n'est jamais envoyé. Le gestionnaire de proxy n'a besoin d'aucun en-tête personnalisé ni d'un Content-Type non sécurisé pour CORS pour fonctionner (il transmet le corps brut, puis écrase le Content-Type sortant en application/json à api/routes.go:254), donc un fetch() cross-origin « simple » (Content-Type: text/plain, aucun en-tête personnalisé) est envoyé par le navigateur sans preflight. La requête facturée côté serveur s'accomplit indépendamment de l'application CORS côté lecture du navigateur.
Effet net : n'importe quelle origine visitée par le navigateur d'une victime, tant que la passerelle est joignable depuis ce navigateur (loopback, LAN, ou public si l'opérateur a suivi le modèle de publication documenté 8080:8080), peut déclencher des complétions de chat arbitraires choisies par l'attaquant via le vrai compte fournisseur de l'opérateur, sans aucune authentification et sans aucune indication visible pour l'utilisateur.
Confirmé dynamiquement de bout en bout avec un vrai navigateur Chrome effectuant une véritable requête cross-origin entre deux origines loopback distinctes (passerelle sur 127.0.0.1, page attaquante sur 127.0.0.2). Voir poc/ :
poc/attacker_site/attack.html — la page exacte servie depuis l'origine attaquante ; sa seule action au chargement est un fetch() vers /proxy/openai/chat/completions.poc/mock_upstream.py — remplace api.openai.com, journalise l'en-tête Authorization, l'Origin et le corps qu'il reçoit.poc/README.md — étapes d'exécution complètes.Observé : la requête cross-origin du navigateur (Origin: http://127.0.0.2:8000) a atteint le mock upstream en portant Authorization: Bearer sk-proj-VICTIM-REAL-BILLED-KEY-... et le corps choisi par l'attaquant {"messages":[{"role":"user","content":"CSRF-DRIVEBY-MARKER-8271"}]} — la page attaquante n'ayant jamais possédé, vu, ou demandé aucun identifiant. L'inspection réseau du navigateur a confirmé que POST http://127.0.0.1:8081/proxy/openai/chat/completions [200] a été déclenché depuis la page 127.0.0.2:8000. Les preuves complètes pilotées par navigateur (journal réseau, capture d'écran) sont jointes à GHSA-5293-fcm6-fh8v.
Tout opérateur exécutant inference-gateway avec sa configuration par défaut documentée voit sa ou ses clés API de fournisseur configurées utilisables par n'importe quelle page web joignable depuis un navigateur pouvant atteindre le port de la passerelle, sans identifiant, cookie, ou position réseau particulière au-delà de « pouvoir envoyer une requête HTTP à l'adresse de la passerelle ». Concrètement : consommation de facturation/quota non autorisée sur le propre compte fournisseur de l'opérateur, déclenchée aveuglément par n'importe quel site tiers, publicité, ou page compromise que l'opérateur (ou quiconque sur le même LAN) a ouverte pendant que la passerelle est en cours d'exécution. Le navigateur empêche l'attaquant de lire la sortie du modèle (aucun en-tête CORS), il s'agit donc d'une transaction forcée aveugle, et non d'une primitive de lecture.
Origin/Sec-Fetch-Site, et sans restriction CORS./health a zéro vérification d'identité par requête lorsque AUTH_ENABLED=false, la valeur par défaut documentée.Corrigé dans v0.46.0 (le mainteneur a durci les valeurs par défaut). Mesures recommandées :
/proxy/:provider/*path (et les autres routes modifiant l'état), ce qui force un preflight CORS pour les appelants cross-origin et donne à la passerelle un endroit pour appliquer une liste d'autorisation d'origines. Cela ferme le contournement par « requête simple » sans exiger l'activation de l'authentification.SERVER_HOST de 0.0.0.0 à 127.0.0.1, exigeant un opt-in explicite pour une interface plus large (comme Ollama l'a fait pour la même classe de vulnérabilité).AUTH_ENABLED=false et que SERVER_HOST n'est pas loopback.README.md / Configurations.md.Dostxodjayev Abdullox (@squeeze440)