La boucle locale n’est pas une frontière de confiance : CVE-2026-2611
Le 19 mai 2026, la National Vulnerability Database a publié CVE-2026-2611. Dans MLflow 3.9.0, la fonctionnalité Assistant validait mal l’origine des requêtes sur ses points de terminaison locaux /ajax-api. Une page malveillante ouverte dans n’importe quel onglet pouvait envoyer des requêtes cross-origin au serveur local de l’Assistant, modifier sa configuration pour activer le mode accès complet, puis exécuter des commandes arbitraires via l’intégration Claude Code CLI fournie avec l’outil. Huntr a attribué à la vulnérabilité un score CVSS 3.0 de 9,6 (CRITICAL), avec le vecteur AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H : accessible par le réseau, faible complexité, aucun privilège requis, périmètre modifié, impact total sur la confidentialité, l’intégrité et la disponibilité.1
La cause profonde n’a rien de nouveau et le correctif n’a rien d’exotique, mais le modèle de menace a changé. Tout service local qui se lie à la boucle locale et expose à un onglet de navigateur des points de terminaison capables de modifier l’état devient une surface de frontière de confiance. Quand ce service pilote un agent IA capable de lire des fichiers, d’exécuter des commandes shell et de pousser du code, « boucle locale uniquement » cesse d’être un contrôle de sécurité. CVE-2026-2611 est un cas d’école pour toute une famille de vulnérabilités qui réapparaîtra à mesure que davantage d’outils de développement intégreront des agents LLM dans des interfaces web locales.
TL;DR
CVE-2026-2611 documente un bug d’énumération CORS incomplète dans MLflow 3.9.0. La fonctionnalité Assistant exposait des points de terminaison /ajax-api/ que le contrôle is_api_endpoint() de MLflow ne couvrait pas, si bien que le middleware de blocage CORS les ignorait. Par-dessus cela, l’ancien CORSMiddleware acceptait allow_origins=["*"]. Une requête cross-origin venue de evil.com pouvait donc appeler /ajax-api/3.0/mlflow/assistant/config, faire basculer l’Assistant en mode accès complet, puis envoyer des commandes shell via l’intégration Claude Code CLI fournie avec l’outil.1 Le correctif amont (PR #20832, « Block CORS for ajax paths ») dans MLflow 3.10.0 ajoute /ajax-api/ à l’énumération des chemins protégés et resserre allow_origins sur la liste d’autorisation configurée, plus une regex localhost.2 Les défenses ci-dessous visent toute la catégorie des agents IA locaux, pas seulement MLflow.
Points clés
| Rôle | Action |
|---|---|
| Ingénieurs AI/ML qui exécutent MLflow | Passez à MLflow 3.10.0 ; auditez toute autre interface locale d’agent IA qui écoute sur la boucle locale |
| Équipes sécurité | Ajoutez « service d’agent LLM local sur boucle locale » à vos modèles de menace ; dans cette catégorie, une seule faille CORS équivaut à une RCE |
| Auteurs d’outils qui construisent des interfaces locales d’agents | Énumérez une seule fois tous les préfixes de chemins API ; interdisez allow_origins=["*"], même sur des serveurs liés à localhost ; conditionnez les points de terminaison destructeurs à un geste utilisateur récent, pas seulement à l’origine |
| Responsables d’ingénierie | Isolez les agents de codage IA de l’équipe et traitez leur capacité à exécuter des commandes comme le véritable rayon d’impact |
À quoi ressemble la chaîne d’attaque
La chaîne tient en cinq étapes. Chacune utilise des capacités autorisées ; c’est leur composition qui échoue.
| Étape | Action | Ce que contrôle l’attaquant |
|---|---|---|
| 1 | L’utilisateur lance mlflow ui localement ; l’Assistant écoute sur la boucle locale (127.0.0.1 par défaut) |
Rien pour l’instant (service local) |
| 2 | L’utilisateur ouvre evil.com dans un onglet pendant que MLflow tourne |
Le contenu de la page |
| 3 | evil.com envoie fetch('http://127.0.0.1:5000/ajax-api/3.0/mlflow/assistant/config', { method: 'PUT', credentials: 'include', body: ... }) |
La requête cross-origin |
| 4 | MLflow accepte la requête parce que /ajax-api/ ne figurait pas dans l’énumération des chemins bloqués par CORS ; la configuration de l’Assistant bascule en accès complet |
La configuration d’exécution de l’Assistant |
| 5 | evil.com publie des charges utiles de commande via le point de terminaison de chat de l’Assistant ; MLflow invoque le Claude Code CLI local avec les contrôles d’autorisation contournés ; les commandes s’exécutent avec les droits de l’utilisateur |
Exécution de code arbitraire |
L’attaquant n’a rien à installer sur la machine de la victime. Il lui suffit que la victime charge un seul onglet contenant la charge utile pendant que mlflow ui est lancé. L’attaque fonctionne depuis n’importe quelle origine, parce que l’ancien CORSMiddleware utilisait allow_origins=["*"] et que le middleware de blocage CORS au niveau des chemins ne couvrait pas le préfixe /ajax-api/.2
L’intégration Claude Code est l’amplificateur de privilèges. Le ClaudeCodeProvider de MLflow localise le CLI claude local via shutil.which("claude") et indique aux utilisateurs de l’installer avec npm install -g @anthropic-ai/claude-code lorsqu’il est absent. Quand l’option de configuration permissions.full_access de l’Assistant est activée, le fournisseur étend l’invocation du CLI avec --permission-mode bypassPermissions, le mode Claude Code qui supprime les demandes interactives d’autorisation et laisse les appels d’outils s’exécuter sans confirmation utilisateur.3 Les capacités autorisées de l’agent (shell, écriture de fichiers, HTTP) deviennent un shell distant avec une UX en langage naturel, exactement ce que cherche un attaquant.
La cause profonde est une énumération incomplète des chemins
Le commit de correction rend le bug évident. security_utils.py dans MLflow contenait une constante :
API_PATH_PREFIX = "/api/"
et un contrôle :
def is_api_endpoint(path: str) -> bool:
return path.startswith(API_PATH_PREFIX) and path not in TEST_ENDPOINTS
Le middleware de blocage CORS appelait is_api_endpoint() pour décider s’il devait appliquer la validation d’origine. Tout chemin qui ne commençait pas par /api/ passait comme chemin non-API. L’Assistant a introduit /ajax-api/ pour ses points de terminaison destinés au navigateur. Personne n’a mis à jour is_api_endpoint() pour inclure ce nouveau préfixe.
Le correctif est un changement de comportement d’une ligne dans security_utils.py :2
API_PATH_PREFIX = "/api/"
AJAX_API_PATH_PREFIX = "/ajax-api/"
def is_api_endpoint(path: str) -> bool:
return (
path.startswith(API_PATH_PREFIX) or path.startswith(AJAX_API_PATH_PREFIX)
) and path not in TEST_ENDPOINTS
Il s’accompagne d’un correctif parallèle dans fastapi_security.py, qui remplace allow_origins=["*"] sur l’ancien CORSMiddleware par la liste d’autorisation configurée, plus une regex localhost. Au total, le changement ajoute 5 lignes et en supprime 2 dans security_utils.py, puis ajoute 5 lignes et en supprime 1 dans fastapi_security.py.2
Voilà toute la vulnérabilité. CVE-2026-2611 existe parce qu’un nouveau préfixe de point de terminaison a été livré sans que la fonction unique qui énumère les chemins protégés par CORS soit mise à jour. La surface d’attaque était une constante de configuration oubliée.
Pourquoi ce motif va se répéter
Le bug est plus ancien que MLflow et plus ancien que les agents IA. Les frameworks web produisent des erreurs d’énumération de chemins depuis que l’énumération de chemins existe. Ce qui a changé, ce sont les conséquences. Une interface web locale qui oubliait d’énumérer un point de terminaison pouvait autrefois divulguer quelques données de configuration ou provoquer un déni de service. Une interface web locale qui oublie d’énumérer un point de terminaison tout en exécutant un agent de codage IA avec accès shell expose toute la machine de développement.
Les interfaces locales d’agents IA forment une catégorie en pleine croissance. Tout outil qui enveloppe un agent LLM dans une interface de navigateur servie depuis 127.0.0.1 entre dans le périmètre : outils de suivi d’expériences avec panneaux de chat (le modèle MLflow), plugins d’IDE pilotant un agent via une API localhost, serveurs en mode développement avec lesquels une extension communique en HTTP, tableaux de bord web pour moteurs de modèles locaux. Chacun se lie à la boucle locale. Chacun expose à un onglet de navigateur des points de terminaison qui modifient l’état. Chacun exécute un agent autorisé à lancer un shell, écrire des fichiers et pousser vers git. Chacun n’est qu’à un bug d’énumération de chemins de CVE-2026-2611.
L’hypothèse commune derrière cette catégorie est la suivante : « la politique same-origin du navigateur protégera le serveur sur boucle locale, puisque les requêtes légitimes viennent aussi de 127.0.0.1 ». Elle s’effondre dès qu’une barrière cross-origin (CORS, validation de l’en-tête Host, validation de l’en-tête Origin, en-têtes Fetch Metadata) manque un préfixe de point de terminaison. Le navigateur ne protège pas le serveur quand le serveur accepte explicitement la requête.
L’attaque par sortie silencieuse que j’ai analysée en mars casse la même hypothèse de confiance dans l’autre sens. La sortie silencieuse dissimule l’exfiltration derrière les outils autorisés de l’agent. La mauvaise classification de la boucle locale dissimule l’entrée derrière les appels réseau autorisés du navigateur. Dans les deux cas, le fond est le même : les capacités autorisées d’un agent LLM deviennent une primitive d’attaque dès qu’un seul contrôle de frontière cède.
À quoi ressemble vraiment cette catégorie
Cette famille de vulnérabilités comporte quatre ingrédients. Un service devient exploitable lorsque les quatre sont réunis :
- Serveur web local lié à la boucle locale ou à
0.0.0.0. La variante limitée à la boucle locale est la plus fréquente. Ce sont les contrôles CORS, Host et Origin du navigateur qui protègent un service lié à localhost contre un onglet cross-origin, pas la liaison réseau. - Points de terminaison de modification accessibles sans geste utilisateur. Changements de configuration, soumissions de commandes, écritures de fichiers. Si un
fetch()depuis une origine arbitraire peut modifier l’état, le service est joignable. - Capacité détenue par l’utilisateur local mais interdite à une page web. Lire le dossier personnel, exécuter des commandes shell, appeler des API cloud avec des identifiants stockés, modifier des fichiers source.
- Un contrôle de frontière manquant. Préfixe de chemin oublié,
allow_originsgénérique, validation absente de l’en-tête Host, validation absente de l’en-tête Origin, absence d’application de Sec-Fetch-Site.
CVE-2026-2611 coche les quatre cases. Le correctif ferme le quatrième ingrédient pour le préfixe /ajax-api/ dans MLflow précisément. Les trois premiers restent présents dans tous les autres outils locaux d’agents IA. Le prochain CVE de cette catégorie aura le même visage, avec un autre nom de package.
Défenses contre la catégorie des agents IA locaux sur boucle locale
Les recommandations CORS classiques (définir allow_origins sur une liste précise, n’activer les identifiants que si nécessaire) sont indispensables, mais insuffisantes. Cette catégorie exige des contrôles plus profonds.
Énumérez les préfixes de chemins à un seul endroit, puis écrivez un test qui échoue dès qu’un nouveau préfixe apparaît sans protection. Le bug de MLflow est arrivé parce que l’équipe a mis à jour is_api_endpoint() lors de l’arrivée de /api/, puis l’a oublié lorsque /ajax-api/ est arrivé. Un test qui parcourt chaque route enregistrée sur l’app FastAPI, demande à is_api_endpoint() si elle est protégée, et échoue sur tout préfixe de chemin non reconnu aurait attrapé le bug au moment de la PR. Le contrôle coûte environ 20 lignes de test.
Validez les en-têtes Origin et Host, pas seulement CORS. CORS protège les navigateurs vis-à-vis de la réponse du serveur. Il ne protège pas le serveur contre l’acceptation de la requête. Les services locaux d’agents IA devraient rejeter les requêtes dont l’en-tête Origin n’appartient pas à la liste d’autorisation explicite et dont l’en-tête Host n’est pas 127.0.0.1 ou localhost (ce dernier point limite les attaques de DNS rebinding). Les deux contrôles doivent échouer fermement.
Conditionnez les points de terminaison destructeurs à Sec-Fetch-Site: same-origin. Les navigateurs envoient Sec-Fetch-Site: cross-site sur tout fetch() cross-origin, y compris de evil.com vers 127.0.0.1. La spécification Fetch Metadata Request Headers définit les valeurs de cet en-tête, et Chrome, Firefox, Safari et Edge l’envoient tous.4 Un middleware qui rejette les requêtes vers les points de terminaison de modification sauf si Sec-Fetch-Site: same-origin ajoute une couche qui ne dépend pas de la mémorisation parfaite de chaque préfixe de chemin.
Isolez la capacité d’exécution de l’agent IA. Claude Code, Cursor, Aider et Continue s’exécutent tous par défaut avec les pleins privilèges de l’utilisateur, parce que c’est l’UX la plus simple. Un bac à sable par espace de travail (bwrap avec des règles de montage explicites sous Linux ; macOS App Sandbox pour les outils distribués en bundles .app ; conteneur par espace de travail pour le multiplateforme) réduit le rayon d’impact sans sacrifier le flux principal. La mitigation ajoute de la friction. Cette friction est le but.
Traitez les commits de l’agent comme non fiables tant qu’ils n’ont pas été relus. Protection de branche, revues obligatoires, commits signés. Le modèle « agent IA avec accès commit » s’effondre dès qu’un attaquant compromet l’environnement local de l’agent. La nécessité de désaccord en revue (traitée dans La revue de code IA a besoin de désaccord) s’applique ici aussi. Si l’agent IA et le relecteur humain sont toujours d’accord, le relecteur humain ne sert que de décor.
À quoi ressemblera probablement le prochain CVE de cette catégorie
La vraie question n’est pas de savoir si une prochaine vulnérabilité en forme de CVE-2026-2611 apparaîtra. La vraie question est de savoir quel package la livrera en premier. Les candidats traversent tout l’écosystème de l’outillage IA local : outils de suivi d’expériences qui intègrent un agent à leur interface, plugins d’IDE qui parlent à un backend local d’agent en HTTP, serveurs en mode développement qu’un outil ouvre pendant une session, API localhost utilisées par des extensions pour piloter des interfaces d’agents. Aucun n’a encore publié de CVE de cette forme. Plusieurs livrent pourtant des chemins de code qui se lient à la boucle locale et acceptent des requêtes cross-origin en laissant le navigateur servir de frontière de confiance.
La catégorie dépasse CVE-2026-2611, et le modèle de menace a changé. En 2026, une machine de développement héberge un agent IA capable de lancer un shell, d’écrire des fichiers, de pousser des commits et d’appeler des API cloud. Les interfaces web locales qui pilotent ces agents héritent des privilèges de l’agent à travers la frontière de confiance que leur serveur HTTP applique. Une mauvaise configuration CORS sur un framework web de 2020 divulguait quelques données de configuration. Une mauvaise configuration CORS sur une interface locale d’agent IA en 2026 expose toute la machine de développement, plus la capacité de déploiement en production.
La solution n’est pas un patch. C’est une famille de contrôles : énumération explicite des chemins avec tests, validation d’en-têtes en défense en profondeur, isolation des capacités, et discipline de revue sur les commits produits par des agents. Les outils qui livrent l’un sans les autres livrent le successeur de CVE-2026-2611 avec un préfixe différent.
FAQ
Qu’est-ce que CVE-2026-2611 ?
CVE-2026-2611 est l’identifiant de la National Vulnerability Database pour une faille de validation d’origine incorrecte dans la fonctionnalité Assistant de MLflow 3.9.0. Cette faille permet à n’importe quelle page web d’envoyer des requêtes cross-origin au point de terminaison local /ajax-api de l’Assistant, de modifier sa configuration pour activer le mode accès complet, puis d’exécuter des commandes arbitraires via l’intégration Claude Code CLI fournie avec l’outil. Score de base CVSS 3.0 : 9,6 (CRITICAL). Corrigé dans MLflow 3.10.0 via la PR #20832, « Block CORS for ajax paths ».12
Quelle était la cause profonde ?
Le contrôle is_api_endpoint() de MLflow n’énumérait que le préfixe de chemin /api/. Quand la fonctionnalité Assistant a introduit des points de terminaison /ajax-api/, personne n’a mis l’énumération à jour. Le middleware de blocage CORS a alors laissé passer les requêtes cross-origin vers /ajax-api/ sans aucun contrôle d’origine. Une seconde faiblesse a aggravé le bug : l’ancien CORSMiddleware autorisait allow_origins=["*"], si bien que même les chemins non bloqués renvoyaient des en-têtes CORS permissifs à n’importe quelle origine. Le correctif ajoute AJAX_API_PATH_PREFIX = "/ajax-api/" au contrôle de chemin et remplace allow_origins=["*"] par la liste d’autorisation configurée, plus une regex localhost.2
Comment savoir si mon installation MLflow est affectée ?
Exécutez pip show mlflow | grep Version. Selon la plage CPE du NVD, les versions MLflow >=3.9.0,<3.10.0 sont affectées. MLflow 3.10.0 (publiée le 20 février 2026) inclut le correctif. Mettez à jour avec pip install --upgrade mlflow, puis redémarrez tous les processus mlflow ui en cours.
L’attaque fonctionne-t-elle sans Claude Code CLI installé ?
L’étape de modification de configuration fonctionne dans tous les cas. L’étape d’exécution de code arbitraire exige que le Claude Code CLI soit présent dans le PATH de la victime, car l’Assistant de MLflow l’invoque via shutil.which("claude"). Sans le CLI, le pire scénario devient une divulgation d’informations et une altération arbitraire de configuration, plutôt qu’une RCE. Pour les utilisateurs qui ont configuré le backend Claude Code (le parcours documenté de configuration de l’Assistant), l’exposition pratique est bien une RCE.
Comment protéger d’autres services locaux d’agents IA ?
Auditez les quatre ingrédients : liaison à la boucle locale ou à une adresse wildcard, points de terminaison de modification, capacités au niveau de l’agent, et tout contrôle de frontière susceptible de comporter des trous. Ajoutez des listes d’autorisation Origin explicites, une validation de l’en-tête Host contre 127.0.0.1/localhost (pour contrer le DNS rebinding), et des barrières Sec-Fetch-Site: same-origin sur les points de terminaison destructeurs. Isolez la capacité d’exécution de l’agent avec bwrap, App Sandbox ou un conteneur par espace de travail. Traitez les commits git de l’agent comme non fiables tant qu’ils n’ont pas été relus.
-
National Vulnerability Database. « CVE-2026-2611 : validation d’origine incorrecte dans MLflow Assistant. » Publié le 19 mai 2026. Score de base CVSS 3.0 de 9,6 (CRITICAL), vecteur
CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H, émis par Huntr. Correctif livré dans MLflow 3.10.0. https://nvd.nist.gov/vuln/detail/CVE-2026-2611. Rapport original : https://huntr.com/bounties/8462addd-b464-4a84-b6a2-5529604e6e5a. ↩↩↩ -
MLflow. « Block CORS for ajax paths (#20832). » Fusionné le 16 février 2026 par Tomu Hirata. Fichiers modifiés :
mlflow/server/fastapi_security.py(+5/-1),mlflow/server/security_utils.py(+5/-2), plus des mises à jour de tests. Le diff remplace le contrôle à préfixe unique deis_api_endpoint()par un contrôle à deux préfixes et resserre leallow_originsde l’ancienCORSMiddleware, de["*"]vers la liste d’autorisation configurée plus une regex localhost. https://github.com/mlflow/mlflow/commit/8f9c8a53af90842944101eb8b7d60706822c81bc. ↩↩↩↩↩↩ -
Source de MLflow 3.10.0.
mlflow/assistant/providers/claude_code.py: recherche viashutil.which("claude")(lignes 268, 280, 355) ; conseil d’installation"Install it with: npm install -g @anthropic-ai/claude-code"(ligne 286) ; escalade du mode d’autorisationcmd.extend(["--permission-mode", "bypassPermissions"])conditionnée àconfig.permissions.full_access(lignes 379–381).mlflow/assistant/config.py: valeur par défautPermissionsConfig.full_access: bool = False(ligne 15).mlflow/server/assistant/api.py: préfixe de route/ajax-api/3.0/mlflow/assistant(ligne 63) ; lecteurGET /config(ligne 261) ; mutateurPUT /config(ligne 276), le point de terminaison PUT étant celui que l’attaque cross-origin cible pour activerfull_access. Vérifié à partir du sdist MLflow 3.10.0 sur PyPI (mlflow-3.10.0.tar.gz). ↩ -
W3C. « Fetch Metadata Request Headers. » Définit
Sec-Fetch-Siteet ses valeurscross-site,same-origin,same-site,none. Implémenté par Chrome 76+, Firefox 90+, Safari 16.4+ et Edge 79+. https://www.w3.org/TR/fetch-metadata/ ↩