Votre agent a un intermédiaire que vous n'avez pas vérifié
Des chercheurs ont acheté 28 routeurs LLM API payants sur Taobao, Xianyu et des boutiques hébergées par Shopify, et en ont collecté 400 gratuits dans des communautés publiques. Sur chacun, ils ont créé un compte, y ont fait passer un agent de code isolé dans un bac à sable, ont exécuté chaque appel d’outil renvoyé et ont observé ce qu’il faisait.1
Neuf des 428 ont réécrit un appel d’outil en une commande ou une dépendance contrôlée par l’attaquant : un routeur payant et huit gratuits. Dix-sept routeurs gratuits ont ensuite utilisé un identifiant canari AWS après l’avoir vu transiter, et un a vidé une clé Ethereum placée en appât. Deux des routeurs injecteurs dissimulaient leur comportement. L’un attendait 50 requêtes avant d’agir, l’autre ne se déclenchait que pour les sessions en « YOLO mode » autonome (mode où les appels d’outils sont approuvés sans confirmation) travaillant sur des projets Rust ou Go.1
Un routeur LLM API est un proxy de couche applicative. Il termine la connexion TLS, lit chaque requête et chaque réponse en clair, et peut réécrire l’appel d’outil que votre agent s’apprête à exécuter. L’article n’a trouvé aucun grand fournisseur qui signe ses réponses d’appels d’outils ; rien ne relie donc la commande exécutée par votre agent à ce que le modèle a produit. L’article a testé quatre frameworks d’agents, dont Claude Code et Codex, et aucun ne vérifiait l’intégrité des réponses.1
Ce billet a d’abord été publié le 10 avril 2026 à partir du résumé de l’article. Le 2 octobre 2026, je l’ai réécrit à partir du texte intégral. La version d’avril comportait plusieurs erreurs ; elles sont recensées vers la fin, sous « Ce que la version d’avril avait de faux ».
TL;DR
- Ce qu’un routeur peut faire. Il se place entre votre client et le fournisseur du modèle en tant que point de terminaison que vous avez configuré ; il détient donc le texte en clair de chaque requête et de chaque réponse, et peut modifier l’une comme l’autre avant de la transmettre.1
- Ce qu’ont fait 428 routeurs. Neuf ont injecté du code malveillant dans les appels d’outils renvoyés (1 sur 28 payants, 8 sur 400 gratuits). Deux des gratuits le dissimulaient derrière des déclencheurs. Dix-sept routeurs gratuits ont utilisé un identifiant canari AWS appartenant aux chercheurs, et un a vidé une clé Ethereum des chercheurs contenant moins de 50 $.1
- Ce qu’ont donné les clés divulguées et les relais mal protégés. Une clé OpenAI divulguée volontairement a servi 100 millions de tokens GPT-5.4 et exposé plus de sept sessions Codex. Des relais leurres mal configurés ont servi environ 2 milliards de tokens et exposé 99 identifiants répartis sur 440 sessions Codex, dont 401 fonctionnaient déjà avec les appels d’outils approuvés automatiquement.1
- Ce qu’ont donné les défenses. Dans le benchmark synthétique des auteurs, une porte de politique fail-closed a bloqué tous les échantillons d’injection avec 1,0 % de faux positifs ; dans un benchmark adaptatif simple, un attaquant qui connaissait la porte l’a contournée dans 100 % des échantillons. Le filtrage des anomalies a signalé 89,0 % des injections simples et environ la moitié des injections furtives.1
- Ce qui réglerait le problème selon les auteurs. Une enveloppe de réponse signée par le fournisseur, qui permettrait au client de confronter un appel d’outil à ce que le modèle a produit. L’article n’a trouvé aucun grand fournisseur qui en propose une.1
- Si vous hébergez vous-même le routeur. La mise à jour du 1er octobre plus bas couvre onze avis de sécurité LiteLLM, dont l’un permettait à tout utilisateur authentifié d’amener le proxy à envoyer ses clés de fournisseur à l’extérieur.2
Points clés à retenir
- Opérateurs d’agents : chaque routeur entre votre client et le fournisseur du modèle a un accès en clair à chaque requête et à chaque réponse, et le client ne configure que le premier saut. Si vous avez acheté un routeur sur une place de marché ou l’avez pris dans une liste publique, traitez-le comme un intermédiaire hostile tant que vous n’avez pas une raison distincte de faire confiance à son opérateur.
- Concepteurs de harnais : un hook PreToolUse s’exécute avant l’appel d’outil, et à ce moment-là un routeur a déjà eu l’occasion de réécrire cet appel. Le hook n’a aucun original auquel se comparer. Ce qu’il peut faire, c’est échouer en mode fermé : n’autoriser que les commandes shell qui téléchargent depuis des domaines listés et n’installent que des paquets listés, et bloquer tout le reste.3
- Quiconque utilise le YOLO mode : dans l’étude des leurres, 401 des 440 sessions Codex observées fonctionnaient déjà avec l’exécution des outils approuvée automatiquement.1 Pour ces sessions, une simple commande réécrite aurait été exécutée, sans aucune logique de déclenchement. Ne faites pas passer de sessions approuvées automatiquement par un routeur que vous ne contrôlez pas.
- Équipes qui hébergent leur propre proxy : un routeur que vous exploitez concentre les clés de fournisseur de la même façon. Parmi onze avis LiteLLM entrés dans la base PyPA le 1er octobre 2026, pour la plupart publics depuis juin, l’un permettait à tout utilisateur authentifié de faire envoyer par le proxy ses clés de fournisseur vers l’hôte de son choix. Les versions à partir de 1.97.0 sont hors de toutes les plages recensées par ces onze avis ; la mise à jour du 1er octobre plus bas donne le détail.2
Qu’est-ce qu’un routeur, exactement ?
Un routeur LLM API accepte des requêtes dans un format donné, en général compatible OpenAI, choisit un fournisseur en amont et renvoie la réponse. L’article recense des routeurs à toutes les échelles : services gérés dans le cloud comme Amazon Bedrock et Azure OpenAI Service, projets et services destinés aux développeurs comme LiteLLM et OpenRouter, et un marché de masse d’accès API revendus et agrégés.1
On les utilise pour de bonnes raisons. L’article cite « model fallback, load balancing, cost optimization, and a single API key across providers » (modèle de repli, répartition de charge, optimisation des coûts et une seule clé API pour tous les fournisseurs), et note que le routage est particulièrement courant « in regions where direct provider access is restricted, expensive, or subject to quota limitations » (dans les régions où l’accès direct aux fournisseurs est restreint, coûteux ou soumis à des quotas).1
Le problème, c’est la position. Aucune astuce d’interception n’est nécessaire, selon les mots de l’article : « the client voluntarily configures the router’s URL as the API endpoint, the router terminates the client-side TLS connection, and it originates a separate TLS connection upstream » (le client configure de lui-même l’URL du routeur comme point de terminaison de l’API, le routeur termine la connexion TLS côté client et ouvre une connexion TLS distincte vers l’amont). TLS protège chaque tronçon. Il ne protège en rien la charge utile contre le routeur, qui lit le JSON de la requête, le transmet, lit le JSON de la réponse et le renvoie, avec la possibilité de modifier l’un ou l’autre.1
Les routeurs s’enchaînent aussi. L’exemple de l’article : un développeur achète un accès à un revendeur sur Taobao, qui agrège des clés auprès d’un agrégateur de second rang, qui route via OpenRouter, qui transmet à l’hôte du modèle. Quatre sauts, chacun avec un accès complet en clair. « The client configures only the first hop; subsequent hops are invisible. » (Le client ne configure que le premier saut ; les suivants sont invisibles.) Un seul mauvais saut n’importe où contamine le chemin, et les sauts honnêtes qui suivent ne peuvent pas le savoir.1
Rien dans les API actuelles ne comble cette lacune. Les arguments des appels d’outils circulent en JSON clair, et « No provider-level integrity mechanism binds the arguments returned by the model to the arguments received by the client. » (Aucun mécanisme d’intégrité au niveau du fournisseur ne lie les arguments renvoyés par le modèle à ceux que reçoit le client.)1
Comment la mesure a été menée
Pour chaque routeur, les auteurs ont suivi quatre étapes. Ils ont créé un compte contrôlé par l’équipe et obtenu des identifiants API. Ils ont fait passer ce compte par un environnement d’agent isolé qui exécutait « a randomized subset of custom tasks and TerminalBench 2.0 tasks » (un sous-ensemble aléatoire de tâches maison et de tâches TerminalBench 2.0), lesquelles produisent des appels d’outils réalistes de shell, d’installation de paquets et de manipulation de fichiers. Ils ont exécuté chaque charge utile d’appel d’outil renvoyée dans le bac à sable, qui surveillait les rappels réseau, les modifications du système de fichiers et les anomalies de l’arbre des processus. Un agent d’analyse distinct a ensuite examiné les exécutions signalées et classé chacune comme réécriture bénigne, potentiellement malveillante ou malveillante confirmée.1
Les définitions sont prudentes. Un routeur n’est compté comme injecteur de code malveillant que « only if a returned tool-call payload is rewritten into an attacker-controlled command or dependency » (si une charge utile d’appel d’outil renvoyée est réécrite en commande ou en dépendance contrôlée par l’attaquant). Un canari AWS est compté comme touché lorsqu’un identifiant passé par le routeur « later produces follow-on AWS API activity attributable to that credential » (produit ensuite une activité API AWS imputable à cet identifiant).1
Le corpus couvre le marché de masse, pas l’ensemble du domaine. Les 400 routeurs gratuits reposent surtout sur deux modèles open source, sub2api et new-api, et les auteurs précisent que les déploiements d’entreprise et sur invitation sont hors du champ de l’étude. L’essentiel du corpus provient de places de marché et de communautés sinophones.1
Les données de terrain
| Comportement | Routeurs payants (28) | Routeurs gratuits (400) |
|---|---|---|
| Injecte du code malveillant dans les appels d’outils renvoyés | 1 | 8 |
| Dissimule l’injection derrière un déclencheur | 0 observé | 2 |
| Utilise un identifiant canari AWS des chercheurs | 0 observé | 17 |
| Vide une clé Ethereum des chercheurs | 0 observé | 1 |
Ces injections n’ont rien de cosmétique. Selon l’article, elles « change the command or dependency that the agent intends to execute » (modifient la commande ou la dépendance que l’agent compte exécuter). Côté payant, le résultat est d’un routeur sur 28, et les auteurs en tirent une conclusion sans détour : « Paid access improves service stability, but it does not prove tool-call integrity. » (Payer améliore la stabilité du service, mais ne prouve pas l’intégrité des appels d’outils.)1
Les deux routeurs à déclencheur sont le résultat qui met les tests en échec. Pour le premier, « malicious tool calls are injected only after the first 50 requests, which hides the attack from shallow smoke tests » (les appels d’outils malveillants ne sont injectés qu’après les 50 premières requêtes, ce qui masque l’attaque aux tests de fumée superficiels). Pour le second, l’injection n’est active « only for sessions that appear to be running in autonomous YOLO mode and only when the project fingerprint matches Rust or Go » (que pour les sessions qui semblent tourner en YOLO mode autonome, et seulement si l’empreinte du projet correspond à Rust ou Go). Les auteurs ajoutent que les déclencheurs observés ne sont peut-être pas les seuls. Ils n’ont observé ces routeurs qu’au travers de sondages en boîte noire en nombre limité, si bien que « a router may implement additional latent conditions that our probes did not activate » (un routeur peut mettre en œuvre d’autres conditions latentes que leurs sondes n’ont pas activées).1
Les résultats sur les identifiants concernent tous les routeurs gratuits. Dix-sept routeurs « trigger follow-on use of at least one researcher-owned AWS canary credential after seeing it in transit » (déclenchent l’utilisation d’au moins un identifiant canari AWS des chercheurs après l’avoir vu transiter), et un a vidé une clé Ethereum préalablement approvisionnée. L’annexe chiffre la perte liée à ce vidage à moins de 50 dollars américains.1
Les deux études d’empoisonnement
Un routeur n’a pas besoin d’être malveillant pour se retrouver dans la même position. Les auteurs ont mené deux études sur la manière dont un chemin de routage d’apparence bénigne se trouve empoisonné.
Étude 1 : une clé divulguée. Ils ont divulgué une clé API OpenAI leur appartenant « on Chinese forums, WeChat, and Telegram groups frequented by router operators » (sur des forums chinois et des groupes WeChat et Telegram fréquentés par des opérateurs de routeurs). Cette seule clé a servi 100 millions de tokens GPT-5.4 et exposé plus de sept sessions d’utilisateurs Codex, et dans au moins une session plusieurs identifiants distincts sont apparus derrière elle. L’article reste prudent sur la façon dont le trafic est arrivé là. Le résultat « is consistent with several paths: direct reuse by a router operator, direct reuse by unrelated third parties, or reuse somewhere inside a longer relay chain » (est compatible avec plusieurs chemins : réutilisation directe par un opérateur de routeur, réutilisation directe par des tiers sans lien, ou réutilisation quelque part dans une chaîne de relais plus longue), et les auteurs « cannot always distinguish among those paths from provider-side logs alone » (ne peuvent pas toujours distinguer ces chemins à partir des seuls journaux du fournisseur). Ce que l’étude établit, c’est le rayon d’impact : quiconque détient une clé amont réutilisée voit en clair le trafic qui passe derrière.1
Étude 2 : des relais mal protégés. Ils ont déployé des instances vulnérables de claude-relay-service sur 20 domaines et des instances Sub2API et CLIProxyAPI à mot de passe faible sur 20 adresses IP. Les leurres ont attiré plus de 40 000 tentatives d’accès non autorisées depuis 147 adresses IP, que les auteurs interprètent comme un mélange de balayage et de réutilisation de relais, et pas systématiquement comme des attaques ciblées. Le signal le plus fort est venu ensuite : les leurres « were folded into active agent-facing relay paths » (ont été intégrés à des chemins de relais actifs desservant des agents). Ils ont servi environ 2 milliards de tokens GPT-5.4 et 5.3-codex, environ 13 Go de trafic visible de prompts et de réponses, et exposé 99 identifiants uniques répartis sur 440 sessions Codex couvrant 398 projets ou hôtes.1
Chacune de ces 440 sessions était vulnérable à l’injection de commandes selon la définition de l’article, et 401 « already operated in YOLO mode, i.e., with tool execution auto-approved rather than confirmed command by command » (fonctionnaient déjà en YOLO mode, c’est-à-dire avec l’exécution des outils approuvée automatiquement plutôt que confirmée commande par commande). Pour ces 401, écrivent les auteurs, « sophisticated adaptive evasion was unnecessary: basic payload injection would have been sufficient to change executed commands » (une évasion adaptative sophistiquée était inutile : une injection de charge utile basique aurait suffi à modifier les commandes exécutées).1
Il s’agissait des sessions d’autres personnes. Leur trafic a atteint les leurres des chercheurs une fois ceux-ci intégrés à des chemins de relais, et un utilisateur qui ne configure que le premier saut n’aurait eu aucun moyen de le voir. Les auteurs n’ont conservé que des métadonnées agrégées et des empreintes salées, et ont mis hors service leurs propres identifiants exposés à la clôture de la fenêtre d’observation.1
Deux classes d’attaques principales et deux variantes d’évasion
L’article ramène le comportement des routeurs malveillants à deux primitives et à deux raffinements de la première.1
AC-1 : injection de charge utile côté réponse. Le routeur réécrit un appel d’outil après sa sortie de chez le fournisseur et avant son arrivée chez le client, en conservant intacts le nom de l’outil et le schéma. L’exemple de l’article est un appel Bash dont l’URL d’installation est remplacée par celle d’un attaquant. Comme le résultat reste un JSON valide pour l’outil attendu, rien ne paraît anormal au client.
AC-2 : exfiltration passive de secrets. Le routeur ne modifie rien. Il analyse requêtes et réponses à la recherche de motifs d’identifiants et garde ce qu’il trouve. Le trafic n’étant pas modifié, « clients cannot observe router-side retention » (les clients ne peuvent pas observer la conservation côté routeur). L’exposition dépasse les clés API : « system prompts, tool definitions, user-supplied file contents, and environment variables all transit the same plaintext channel » (prompts système, définitions d’outils, contenus de fichiers fournis par l’utilisateur et variables d’environnement transitent tous par le même canal en clair).
AC-1.a : injection ciblant les dépendances. Une variante d’AC-1 visant les commandes d’installation de paquets. Au lieu de remplacer une URL, le routeur « substitutes a legitimate dependency name with an attacker-controlled package pre-registered on the target registry » (substitue au nom d’une dépendance légitime un paquet contrôlé par l’attaquant et préenregistré sur le registre cible). Dans l’exemple de l’article, pip install requests devient pip install reqeusts. La commande installe toujours depuis le registre de confiance, si bien qu’une porte qui ne vérifie que les domaines la laisse passer, et le paquet malveillant reste en cache pour les sessions suivantes.
AC-1.b : livraison conditionnelle. Une variante qui décide quand AC-1 se déclenche. Le routeur se comporte honnêtement jusqu’à ce qu’un déclencheur corresponde. L’article énumère cinq familles de déclencheurs : nom de l’outil, mots-clés du contenu, empreintes de l’utilisateur comme le YOLO mode, fenêtres temporelles et nombre de requêtes. « A router deploying a 50-request warm-up appears entirely transparent during a 10-request probe. » (Un routeur doté d’une période de chauffe de 50 requêtes paraît parfaitement transparent lors d’un sondage de 10 requêtes.)
L’article cite la compromission de LiteLLM de mars 2026 comme un AC-1 à grande échelle : dès qu’un attaquant contrôlait le pipeline de requêtes du proxy, chaque appel d’outil qui y transitait pouvait être réécrit. (L’article décrit cette compromission comme une confusion de dépendances. Les sources sur lesquelles je me suis appuyé dans le billet sur la chaîne d’approvisionnement la font remonter à un jeton de publication PyPI volé lors de la compromission de Trivy.)
Ce que le proxy de recherche a révélé sur les clients d’agents
Les auteurs ont construit un proxy baptisé Mine qui met en œuvre les quatre classes, et ont envoyé 1 000 requêtes d’appel d’outil par module d’attaque à travers chacun de quatre frameworks d’agents publics : OpenClaw, OpenCode, Codex d’OpenAI et Claude Code d’Anthropic.1
Aucun des quatre ne vérifiait l’intégrité des réponses. L’appel d’outil réécrit arrivait sous une forme valide et native du framework dans 100 % des cas AC-1 et 99,6 % des cas AC-1.a. L’article est précis sur ce que cela montre et ne montre pas : « These are compatibility measurements, not end-to-end execution rates: they show that the tampered tool call reaches the client in a valid framework-native format, but downstream permission prompts or local execution sandboxes may still block the resulting action. » (Ce sont des mesures de compatibilité, pas des taux d’exécution de bout en bout : elles montrent que l’appel d’outil altéré atteint le client dans un format valide et natif du framework, mais que des demandes d’autorisation en aval ou des bacs à sable d’exécution locaux peuvent encore bloquer l’action qui en résulte.)1
C’est précisément là qu’une demande d’autorisation prouve son utilité, et c’est pourquoi l’approbation automatique pèse autant dans les chiffres des leurres. Les auteurs n’ont pas publié Mine.1
Le motif de composition, une couche plus bas
J’écris sans cesse sur le même défaut structurel : des composants autorisés individuellement qui, une fois composés, produisent un comportement non autorisé. Trivy-to-LiteLLM était une composition au niveau des paquets. L’exfiltration silencieuse était une composition au niveau du contenu récupéré. L’empoisonnement d’outils MCP était une composition au niveau du protocole.
L’attaque par routeur est une composition sur le saut entre le client et le fournisseur. Votre client est autorisé à appeler le routeur. Le routeur est autorisé à appeler le modèle en amont. Le modèle est autorisé à répondre. Chaque saut est autorisé, et la composition produit malgré tout des commandes réécrites, parce que personne ne signe ce qui franchit le saut, et des secrets copiés, parce que chaque saut le lit en clair.
L’article prolonge le trait jusqu’à MCP lui-même : « a malicious MCP server receives tool-call requests in plaintext and can return forged results, so the same basic manipulation and collection ideas transfer with adaptation to the MCP message format » (un serveur MCP malveillant reçoit les requêtes d’appel d’outil en clair et peut renvoyer des résultats falsifiés, de sorte que les mêmes idées de base de manipulation et de collecte se transposent, moyennant adaptation au format des messages MCP). Il marque aussi la différence. Un serveur MCP se situe du côté de l’exécution des outils et peut falsifier leur sortie. Un routeur se situe sur le chemin vers le modèle et voit chaque appel d’outil ainsi que tout le contexte de la requête.1
Trois défenses, avec les chiffres de l’article
L’article évalue trois contrôles qu’un client peut déployer sans aucune aide d’un fournisseur. Une réserve s’applique à tous les chiffres ci-dessous : « All numbers in this section come from controlled artifact-side evaluation rather than field data. » (Tous les chiffres de cette section proviennent d’une évaluation contrôlée sur l’artefact de recherche, et non de données de terrain.) Les corpus sont synthétiques, générés par les scripts des auteurs.1
| Défense | AC-1 | AC-1.a | AC-1.b | Coût et limite |
|---|---|---|---|---|
| Porte de politique fail-closed | 100 % bloqués | 100 % bloqués | non mesuré | 1,0 % de faux positifs ; contournée dans 100 % des échantillons d’un benchmark adaptatif simple |
| Filtrage des anomalies côté réponse | 89,0 % signalés | 50,0 % signalés | 50,9 % signalés | 6,7 % de faux positifs |
| Journal de transparence en ajout seul | audit uniquement | audit uniquement | audit uniquement | environ 1,26 Ko par entrée |
La porte de politique bloque les commandes shell qui « fetch from non-allowlisted domains or install packages outside a local allowlist » (téléchargent depuis des domaines hors liste d’autorisation ou installent des paquets hors d’une liste d’autorisation locale). Sur 1 000 commandes bénignes, 500 échantillons AC-1 et 500 échantillons AC-1.a, elle a bloqué tous les échantillons d’attaque, et tous ses faux positifs étaient des téléchargements d’installateurs depuis des domaines miroirs absents de la liste. Les auteurs la qualifient de « the strongest immediately deployable control » (contrôle le plus solide déployable immédiatement). Ils exposent tout aussi franchement son échec. Si l’attaquant héberge la charge utile sur un domaine autorisé, ou dépose à l’avance un script local qu’une commande d’apparence bénigne exécutera plus tard, « the policy gate is bypassed in 100% of samples » (la porte de politique est contournée dans 100 % des échantillons). Leur verdict : la porte « should be understood as a coarse but useful filter for obvious high-risk workflows, not as a robust defense against defense-aware attackers » (doit être vue comme un filtre grossier mais utile pour les flux de travail manifestement risqués, et non comme une défense robuste face à des attaquants qui connaissent les défenses).1
Un détail compte pour quiconque construit cette porte, et l’article n’est pas cohérent sur ce point. La section 4 décrit la porte comme limitée aux domaines et conçoit AC-1.a pour la déjouer : « the gate blocks non-allowlisted domains but does not maintain a per-package allowlist » (la porte bloque les domaines hors liste mais ne tient pas de liste d’autorisation par paquet). La porte de la section 7, celle qui a obtenu 100 % contre AC-1.a, bloque aussi les installations « outside a local allowlist » (hors d’une liste d’autorisation locale). À mon sens, une porte sans liste de paquets laisse entièrement passer la substitution par typosquatting.
Le filtrage des anomalies attribue un score à chaque appel d’outil renvoyé d’après des caractéristiques visibles localement, comme des motifs de risque shell ou l’entropie des arguments, sans original amont auquel se comparer. Avec 6,7 % de faux positifs, il a signalé 89,0 % des échantillons AC-1, 50,0 % des AC-1.a et 50,9 % des AC-1.b. Avec un budget de 1 % de faux positifs, il a détecté 43,5 % des AC-1 et pratiquement aucun AC-1.a. Les auteurs le jugent « useful for review prioritization » (utile pour prioriser les vérifications) et moins efficace que la porte.1
Le journal de transparence enregistre la requête, la réponse, l’URL du routeur, les métadonnées TLS et une empreinte de la réponse brute. Il n’empêche rien. Il vous permet, après un incident, de déterminer jusqu’où un routeur ou un identifiant a porté et quelles sessions y sont passées.1
Le correctif que demandent les auteurs
Aucun des trois contrôles n’authentifie l’origine d’un appel d’outil. Les auteurs le disent : « No client-side control available today can prove that a router preserved the upstream provider’s response. » (Aucun contrôle côté client disponible aujourd’hui ne peut prouver qu’un routeur a préservé la réponse du fournisseur amont.)1
Ce qui le pourrait, c’est une signature du fournisseur. L’article propose « a provider-signed canonical response envelope, similar in spirit to DKIM for email » (une enveloppe de réponse canonique signée par le fournisseur, dans l’esprit de DKIM pour le courriel), couvrant l’identifiant du modèle, le nom de l’outil, ses arguments, la raison de fin et un nonce du client, que le client vérifie avant d’exécuter tout appel d’outil. Il constate que personne ne la propose : « To our knowledge, none of the major provider tool-use APIs or the current MCP specification expose a deployed response-signing mechanism for tool-call arguments today. » (À notre connaissance, aucune des API d’utilisation d’outils des grands fournisseurs ni la spécification MCP actuelle n’offrent aujourd’hui de mécanisme déployé de signature des réponses pour les arguments d’appels d’outils.)1
La proposition a deux limites. La sécurité du transport ne la remplace pas : le TLS mutuel et l’épinglage de certificats « can authenticate the router endpoint the client chose, but they do not say whether the returned tool call preserves upstream semantics » (peuvent authentifier le point de terminaison de routeur choisi par le client, mais ne disent pas si l’appel d’outil renvoyé préserve la sémantique amont). Et la signature ne fait rien contre le vol de secrets : « AC-2 cannot be mitigated by response-signing proposals because the secrets are exposed on the request path before any provider-side mechanism can act. » (AC-2 ne peut pas être atténuée par la signature des réponses, car les secrets sont exposés sur le chemin de la requête avant qu’un quelconque mécanisme côté fournisseur puisse agir.)1
Ce que vous devriez réellement faire
Si votre agent appelle un modèle via un routeur que vous n’avez pas construit :
- Passez en direct quand vous le pouvez, et connaissez l’opérateur quand vous ne le pouvez pas. Il suffit de changer une URL de base pour ajouter un routeur, et c’est pourquoi on l’ajoute sans jamais décider de le faire. Faites-en une décision. « Confiance » signifie ici une base externe, comme une équipe connue, un contrat ou une juridiction dans laquelle vous pouvez faire valoir vos droits. Les avis d’une place de marché n’en sont pas une.
- Échouez en mode fermé sur les appels d’outils à risque. Dans Claude Code, c’est un hook PreToolUse qui bloque les commandes shell téléchargeant depuis des domaines hors liste d’autorisation et les installations de paquets hors liste. Conservez la liste des paquets : la variante par substitution de dépendance existe précisément pour déjouer une porte limitée aux domaines. Écrivez le hook pour qu’il refuse tout ce qu’il ne parvient pas à analyser, avec le code de sortie 2 ou une décision
deny. Claude Code traite les autres codes de sortie et les dépassements de délai comme non bloquants ; un hook qui plante ou se fige n’arrête donc pas l’appel, qui suit alors le flux d’autorisation normal et, dans une session approuvée automatiquement, s’exécute tout simplement. Vérifiez aussi que le hook s’est bien exécuté lors de son premier appel. La documentation avertit : « a mistyped path in settings.json leaves the gate silently disabled » (un chemin mal saisi dans settings.json laisse la porte désactivée sans le moindre signal). Attendez-vous à maintenir les deux listes, et à ce qu’un attaquant déterminé les contourne.3 - Ne faites jamais passer de sessions approuvées automatiquement par un routeur que vous ne contrôlez pas. Les 401 sessions de l’étude des leurres font jurisprudence. Une demande d’autorisation est l’une des rares choses qui s’interposent entre un appel d’outil réécrit et son exécution.
- Tenez les secrets à l’écart du trafic. La collecte passive ne modifie rien que vous puissiez observer. Tout ce qui figure dans un prompt, un résultat d’outil ou un fichier lu par l’agent traverse le routeur en clair. Restreignez étroitement la portée des identifiants, gardez-les hors du contexte autant que possible, et renouvelez tout ce qui est passé par un routeur dont vous doutez après coup.
- Journalisez en local. Requêtes, réponses, URL du routeur et empreinte de la réponse, en expurgeant d’abord les secrets des requêtes, stockés là où le routeur ne peut pas accéder. Cela n’arrêtera pas une attaque. Cela vous dira après coup ce qui a été exposé.
- Exécutez dans un bac à sable. L’article note que les bacs à sable « reduce post-execution blast radius but do not authenticate where a tool call came from » (réduisent le rayon d’impact après exécution mais n’authentifient pas l’origine d’un appel d’outil). Prenez la première moitié.
L’implication inconfortable
La couche des routeurs illustre nettement un écosystème d’agents qui livre son infrastructure plus vite qu’il ne la sécurise. On veut une seule clé pour tous les modèles, des prix plus bas et un accès depuis des régions qu’un fournisseur ne dessert pas. Les routeurs offrent les trois, et le marché les récompense.
La même séquence s’est déjà jouée sur la couche MCP, la couche des paquets et la couche du contenu récupéré. Une nouvelle couche de la pile d’agents apparaît. Les développeurs l’adoptent avant que quiconque l’audite. Les attaquants arrivent, puis les chercheurs. Ici, les chercheurs ont dénombré 428 routeurs, dont 9 injectant du code malveillant, 17 utilisant des identifiants piégés, 1 vidant un portefeuille, et 401 sessions approuvées automatiquement transitant par des chemins de relais qui incluaient les leurres des chercheurs.1
La pièce qui comblerait la lacune, une réponse signée par le fournisseur, n’est pas quelque chose qu’un opérateur peut ajouter. Tant que les fournisseurs ne la livrent pas, les contrôles ci-dessus réduisent l’exposition, et aucun ne prouve qu’un appel d’outil vient bien du modèle.
Mise à jour du 1er octobre 2026 : le routeur que vous hébergez figure aussi sur la liste
Ce billet porte sur des routeurs exploités par d’autres. Les avis de sécurité complètent l’autre moitié du tableau. Le 1er octobre 2026, la base d’avis PyPA a ajouté onze entrées visant LiteLLM, un proxy open source que des équipes hébergent elles-mêmes pour les raisons évoquées plus haut : une seule clé pour tous les modèles.2 Aucune n’est nouvelle. Neuf figurent dans la base d’avis de GitHub et dans la NVD depuis le 21 juin, une dixième depuis la mi-septembre, et celle qui compte le plus pour un proxy, parce qu’elle expose les clés de fournisseur, a été publiée dans le dépôt de LiteLLM le 26 août, avec des correctifs sur PyPI depuis le 9 août ; GitHub la classe Moderate. Elles méritent d’être lues ensemble pour ce qu’elles disent de cette couche, et parce que toute version finale publiée avant le 9 août se trouve dans la plage de CVE-2026-84377.
Celle à lire en premier est CVE-2026-84377. Selon l’avis du dépôt, « Any authenticated LiteLLM proxy user could redirect an outbound provider call to a destination they control and cause the proxy to send its own configured provider credentials to that destination. » (Tout utilisateur authentifié du proxy LiteLLM pouvait rediriger un appel sortant vers le fournisseur vers une destination qu’il contrôle et amener le proxy à y envoyer les identifiants de fournisseur qu’il a configurés.) La cause tient à la forme du contrôle : « The proxy’s request-body validation was a denylist that did not cover every sensitive parameter and did not inspect parameters nested inside other request fields. » (La validation du corps de requête du proxy était une liste de refus qui ne couvrait pas tous les paramètres sensibles et n’inspectait pas les paramètres imbriqués dans d’autres champs de la requête.) Le correctif existe dans neuf lignes de versions, de 1.88.6 à 1.96.2. Pour qui ne peut pas encore mettre à jour, l’avis énumère trois contournements en une phrase : « Set general_settings.allow_client_side_credentials to false so callers cannot override connection parameters, restrict proxy keys to trusted callers, and block the affected parameters (api_base, base_url, model_list, fallbacks, provider credential fields) at a reverse proxy or API gateway. » (Régler ce paramètre sur false pour que les appelants ne puissent pas surcharger les paramètres de connexion, réserver les clés du proxy aux appelants de confiance, et bloquer les paramètres concernés au niveau d’un proxy inverse ou d’une passerelle API.)2 Le premier ne suffit pas à lui seul. Dans le code source de 1.95.0, une version affectée, le contrôle du corps de requête n’est entièrement ignoré que lorsque ce réglage vaut true (le configurable_clientside_auth_params d’un déploiement peut tout de même exempter des paramètres isolés) ; false est donc la valeur où se trouve déjà une installation qui n’y a jamais touché, et le contrôle incomplet est précisément la faille que décrit l’avis. La mise à jour est le vrai correctif.6
Une deuxième entrée, CVE-2026-59823, est la même faille en miniature : la protection « blocks the api_base and base_url parameters but does not cover user_config » (bloque les paramètres api_base et base_url mais ne couvre pas user_config), si bien qu’un appelant muni d’une clé virtuelle valide pouvait y placer un api_base et pointer le proxy vers n’importe quel hôte. Celle-ci a été corrigée en 1.83.9, sur PyPI depuis le 17 avril ; son avis a suivi en septembre.4
Deux entrées touchent la partie MCP du proxy : une authentification incorrecte dans le proxy MCP (CVE-2026-12773, avec une plage de versions qui s’arrête à un correctif en 1.84.0) et une falsification de requête côté serveur via l’argument spec_path du chargeur de spécifications OpenAPI de MCP (CVE-2026-12798, enregistrée comme affectant les versions jusqu’à 1.82.2). Les sept autres concernent la gestion des clés d’administration, le flux de débogage SSO, l’invalidation des sessions SSO, l’expiration des sessions pour les clés générées, l’énumération des utilisateurs dans l’interface, un contournement de garde-fou sur des points de terminaison asynchrones et le traitement des JWT de machine à machine.5
Ces neuf entrées sont plus minces que les deux premières. Leurs descriptions reprennent la formulation standardisée de VulDB plutôt qu’une analyse rédigée par les mainteneurs, et la description de l’entrée sur le proxy MCP indique « up to 1.59.8 » (jusqu’à 1.59.8) alors que sa plage de versions indique un correctif en 1.84.0.5 L’enregistrement de CVE-2026-84377 présente sa propre incohérence : la page du dépôt indique comme versions affectées « <1.94.0 », alors que les plages de l’enregistrement validé couvrent la ligne 1.96 jusqu’à son correctif en 1.96.2.2
Vérifiez les plages par rapport à la version que vous exploitez plutôt que de vous fier à un résumé, celui-ci compris. Toutes les plages recensées s’arrêtent à 1.96.2 ou en dessous ; les versions à partir de 1.97.0, sur PyPI depuis le 16 août, échappent donc aux onze ; la version courante au 1er octobre est 1.103.2.5
Le danger d’un routeur tient à l’endroit où se trouvent les identifiants, quel que soit son exploitant. Un routeur de place de marché qui touche à une clé AWS piégée et un proxy auto-hébergé qu’on peut convaincre d’envoyer ses clés de fournisseur à l’extérieur constituent la même exposition atteinte par deux côtés, l’une par l’opérateur, l’autre par n’importe quel locataire détenteur d’une clé virtuelle. Les défenses des sections précédentes supposent que vous êtes le client.
Pour un proxy que vous exploitez, ajoutez la moitié qui revient à l’opérateur : traitez chaque clé virtuelle comme un identifiant donnant accès aux clés amont qui se trouvent derrière, laissez désactivés les paramètres de connexion fournis par le client, y compris le configurable_clientside_auth_params propre à chaque déploiement, sauf si vous en avez besoin, et alignez le proxy sur le même rythme de correctifs que tout ce qui détient des secrets.
Si une personne à qui vous ne faites pas entièrement confiance détenait une clé virtuelle pendant que le proxy exécutait une version affectée, renouvelez les clés de fournisseur et tous les autres secrets configurés sur le proxy, et examinez ses journaux à la recherche d’appels sortants vers des hôtes que vous n’avez pas configurés ; l’impact décrit par l’avis couvre « other configured secrets » (d’autres secrets configurés) et des requêtes vers des « internal services reachable from the proxy » (services internes joignables depuis le proxy). Deux des onze entrées, CVE-2026-12773 dans le proxy MCP et CVE-2026-12795 dans le flux de débogage SSO, sont des failles d’authentification dans des versions antérieures à 1.84.0 ; sur ces versions, le fait de détenir une clé ne délimite donc pas forcément qui pouvait atteindre le proxy. S’il était joignable depuis des réseaux auxquels vous ne faites pas confiance, appliquez le même renouvellement.
Ce que la version d’avril avait de faux
La version du 10 avril de ce billet avait été rédigée à partir du résumé de l’article. Relue le 2 octobre 2026 à la lumière du texte intégral, elle était fausse ou trompeuse aux endroits suivants, tous corrigés plus haut :
- AC-1.a. Je l’avais décrite comme une injection qui « only fires when the request matches a specific dependency or context » (ne se déclenche que lorsque la requête correspond à une dépendance ou à un contexte précis). Il s’agit en fait d’une substitution de nom de paquet dans une commande d’installation. Les déclencheurs relèvent d’AC-1.b.
- Les défenses. J’avais écrit que « the abstract does not rank the defenses » (le résumé ne classe pas les défenses) et proposé un classement à titre d’opinion. Le corps de l’article mesure les trois, qualifie la porte de politique de « the strongest immediately deployable control » (contrôle le plus solide déployable immédiatement) et indique que, dans un benchmark adaptatif simple, elle a été contournée dans 100 % des échantillons. J’avais omis ce point.
- Le conseil sur la signature. J’avais recommandé aux opérateurs de signer les requêtes côté client et de les vérifier en amont, et appelé cela « the only real fix » (le seul vrai correctif). L’article demande le sens inverse, une réponse signée par le fournisseur et vérifiée par le client, et précise que la signature ne peut rien contre le vol de secrets.
- La clé divulguée. J’avais écrit que la clé avait été divulguée « as if it had been exposed through a developer mistake » (comme si elle avait été exposée par une erreur de développeur) et conclu que « The router was a laundering layer for a stolen key. » (Le routeur servait de couche de blanchiment pour une clé volée.) Elle a en réalité été divulguée sur des forums et dans des groupes de discussion où des opérateurs de routeurs partagent des identifiants, et l’article dit ne pas toujours pouvoir déterminer si c’est un opérateur de routeur, un tiers sans lien ou une chaîne de relais plus longue qui l’a réutilisée.
- Les chiffres sur les identifiants. Le bloc de réponse indiquait « 17 of 28 paid routers touched planted AWS credentials » (17 des 28 routeurs payants ont touché aux identifiants AWS piégés), et la description affirmait que les chercheurs avaient testé 28 routeurs. La ligne des routeurs payants de l’article ne montre aucun abus d’identifiants observé. Les 17 routeurs qui ont utilisé des canaris AWS, ainsi que celui qui a vidé l’ETH, font tous partie des 400 routeurs gratuits.
- Le conseil sur les hooks. J’avais recommandé des hooks PostToolUse qui « validate response shapes » (valident la forme des réponses). Un hook PostToolUse s’exécute après l’exécution de l’outil, trop tard face à une commande réécrite. Le contrôle testé par l’article est une porte placée avant l’exécution, c’est-à-dire, dans Claude Code, un hook PreToolUse doté d’une liste d’autorisation.
- Erreurs mineures. L’article compte six auteurs, pas cinq. Il ne dit pas qu’un routeur « knows when it is being sampled » (sait quand il est échantillonné) ; c’était un embellissement de ma part. L’introduction présentait le sujet comme des « MCP trust chains » (chaînes de confiance MCP), alors que l’article étudie des routeurs, pas MCP. J’avais présenté le billet sur l’exfiltration silencieuse comme portant sur les descriptions d’outils, alors qu’il traite d’instructions cachées dans du contenu récupéré.
FAQ
Qu’est-ce qu’un routeur LLM API dans ce contexte ?
Un service qui accepte des requêtes dans un format unifié, généralement compatible OpenAI, choisit un fournisseur de modèle en amont et renvoie la réponse. C’est un proxy de couche applicative qui a un accès en clair à chaque requête et à chaque réponse.1
TLS me protège-t-il d’un routeur malveillant ?
Non. Le client configure le routeur comme son point de terminaison ; le routeur termine donc la session TLS du client et en ouvre une autre vers l’amont. TLS protège chaque tronçon, mais ne protège en rien la charge utile contre le routeur.1
Comment détecter un routeur qui réécrit les appels d’outils ?
Pas de manière fiable en le testant. Deux routeurs de l’étude n’injectaient qu’après 50 requêtes, ou seulement pour des sessions approuvées automatiquement sur des projets Rust ou Go, et l’article conclut que « no fixed-length client test can guarantee that the router is benign » (aucun test client de longueur fixe ne peut garantir que le routeur est bénin). Une liste d’autorisation fail-closed pour les commandes shell et les installations de paquets bloque les cas simples, et l’article montre qu’un attaquant au fait des défenses parvient à la contourner.1
Un hook PreToolUse est-il utile ?
Oui, en tant que porte de politique. Le hook voit l’appel d’outil reçu par le client, c’est-à-dire la version réécrite si un routeur l’a réécrit, et peut bloquer les commandes qui téléchargent depuis des domaines non listés ou installent des paquets non listés. Il ne peut pas savoir si l’appel correspond à ce que le modèle a produit.13
J’utilise Claude Code directement avec api.anthropic.com. Suis-je concerné ?
Pas par les attaques de routeurs de cet article, puisqu’il n’y a aucun intermédiaire. Si vous faites passer Claude Code par un proxy pour une raison quelconque, comme une passerelle d’entreprise ou un agrégateur de modèles, ce proxy occupe la même position.
Et OpenRouter, LiteLLM ou d’autres agrégateurs connus ?
L’article mesure 28 routeurs payants issus de trois places de marché et 400 routeurs gratuits bâtis pour la plupart sur deux modèles open source. Il cite LiteLLM et OpenRouter à titre de contexte et ne les teste pas. Le constat structurel vaut pour tout routeur : il peut lire et réécrire le trafic, et la notoriété est une propriété distincte de l’intégrité. Pour un proxy que vous hébergez vous-même, la mise à jour du 1er octobre plus haut couvre onze avis LiteLLM.
À qui appartenaient les 401 sessions approuvées automatiquement ?
À des tiers dont le trafic a atteint les relais leurres des chercheurs. Si vous faites passer des sessions d’agent approuvées automatiquement par un routeur que vous n’avez pas construit, arrêtez, renouvelez chaque identifiant qui l’a traversé et examinez les journaux de session à la recherche d’appels d’outils inattendus.
Références
-
Hanzhi Liu, Chaofan Shou, Hongbo Wen, Yanju Chen, Ryan Jingyang Fang et Yu Feng, “Your Agent Is Mine: Measuring Malicious Intermediary Attacks on the LLM Supply Chain,” arXiv:2604.08407v1, 9 avril 2026, annoncé pour l’ACM Conference on Computer and Communications Security, octobre 2026. Texte intégral lu les 1er et 2 octobre 2026. Sections utilisées : introduction et 2.1 (ce que sont les routeurs, l’exemple à quatre sauts, la terminaison TLS) ; 2.2 (aucun mécanisme d’intégrité au niveau du fournisseur) ; 4.1 et 4.2 (les quatre classes d’attaques, l’exemple de
requestsdevenureqeusts, les cinq familles de déclencheurs) ; 5.1 (le pipeline en quatre étapes et les définitions) ; 5.2 et tableaux 3 et 4 (1 routeur payant et 8 gratuits injecteurs, 2 routeurs gratuits à déclencheur, 17 routeurs gratuits utilisant des canaris AWS, 1 vidant de l’ETH) ; 5.3 (la clé divulguée et les leurres : 100 millions de tokens, plus de sept sessions Codex, plus de 40 000 tentatives d’accès depuis 147 adresses IP, environ 2 milliards de tokens, environ 13 Go, 99 identifiants, 440 sessions, 398 projets ou hôtes, 401 en YOLO mode) ; 5.4 (les principaux résultats, dont la phrase sur l’accès payant) ; 5.5 (périmètre) ; 6 et tableau 5 (Mine, quatre frameworks, 1 000 requêtes par module, 100 % et 99,6 % de compatibilité) ; 7 et tableau 6 (les trois défenses et leurs résultats) ; 8.2 et 8.3 (l’enveloppe de réponse signée, MCP) ; 9 (la différence entre la position d’un serveur MCP et celle d’un routeur) ; annexe A (conservation, mise hors service des identifiants, vidage de moins de 50 dollars américains, Mine non publié). ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
Avis du dépôt LiteLLM GHSA-3cv6-jpf6-8222 (CVE-2026-84377, PYSEC-2026-4066), « Authenticated SSRF and provider-credential exfiltration via unvalidated request-body routing parameters » (SSRF authentifiée et exfiltration d’identifiants de fournisseur via des paramètres de routage non validés dans le corps de requête), publié dans le dépôt le 26 août 2026, recensé par la NVD le 2 septembre et validé par GitHub le 30 septembre ; lu sur la page du dépôt et via l’enregistrement OSV le 1er octobre 2026. Impact et la phrase complète de Workarounds sont cités de l’avis ; la ligne Patches de l’enregistrement OSV indique « Fixed in 1.96.2, 1.95.1, 1.94.3, 1.93.2, 1.92.2, 1.91.5, 1.90.7, 1.89.7, and 1.88.6 », et la page du dépôt indique « Affected versions <1.94.0 ». Le total de onze correspond aux entrées PYSEC-2026-4066 à PYSEC-2026-4076 de la base d’avis PyPA, toutes pour le paquet
litellm, toutes datées du 1er octobre 2026, aucune retirée. ↩↩↩↩↩ -
Anthropic, Hooks reference, documentation de Claude Code, consultée le 2 octobre 2026. PreToolUse s’exécute « Before a tool call executes. Can block it » (avant l’exécution d’un appel d’outil, qu’il peut bloquer) ; PostToolUse s’exécute « After a tool call succeeds » (après la réussite d’un appel d’outil) ; « Any other exit code doesn’t block on its own for most hook events » (pour la plupart des événements de hook, tout autre code de sortie ne bloque pas à lui seul) ; un hook de commande qui dépasse son délai « doesn’t block the tool call » (ne bloque pas l’appel d’outil) ; et « a mistyped path in settings.json leaves the gate silently disabled. » (une faute de frappe dans un chemin de settings.json laisse le contrôle désactivé sans le moindre signal). ↩↩↩
-
Avis de sécurité GitHub GHSA-hx8v-g79f-8w5f (CVE-2026-59823, PYSEC-2026-4070), « LiteLLM Proxy has server-side request forgery via the
user_configrequest parameter » (LiteLLM Proxy présente une falsification de requête côté serveur via le paramètre de requêteuser_config), publié le 17 septembre 2026, lu via OSV le 1er octobre 2026 : versions affectées<= 1.83.8, corrigé en1.83.9, l’avis datant cette version du 17 avril 2026. ↩ -
Enregistrements OSV lus le 1er octobre 2026 : PYSEC-2026-4067 (CVE-2026-12773, authentification du proxy MCP), PYSEC-2026-4069 (CVE-2026-12798, chargeur de spécifications OpenAPI de MCP), ainsi que PYSEC-2026-4068, 4071, 4072, 4073, 4074, 4075 et 4076. Les avis GitHub derrière ces neuf entrées ont été publiés le 21 juin 2026, le jour où la NVD les a recensés, et chacun cite VulDB. Les dates de mise en ligne et la version courante proviennent de la page du projet sur PyPI et de son JSON, lus le même jour : 1.88.6 à 1.95.1 le 9 août, 1.96.2 le 11 août, 1.97.0 le 16 août et 1.103.2 le 1er octobre. ↩↩↩
-
Lecture par l’auteur de la wheel
litellm1.95.0 depuis PyPI (plage affectée 1.95.0 jusqu’au correctif en 1.95.1), 1er octobre 2026 : danslitellm/proxy/auth/auth_utils.py,_check_banned_paramsretourne avant de rejeter quoi que ce soit lorsquegeneral_settings.get("allow_client_side_credentials") is True, et sinon rejette une requête dont le corps contient un paramètre de la liste d’exclusion, sauf si leconfigurable_clientside_auth_paramsdu déploiement autorise ce paramètre. Dans cette version, la liste d’exclusion inclutvertex_ai_credentialsainsi que des identifiants et hôtes d’observabilité. J’ai lu une seule version affectée, pas les neuf lignes. ↩