Le notebook apportait son propre serveur MCP
CVE-2026-75149 est une faille d'injection de code dans marimo : un notebook est un unique fichier Python, ce fichier transporte sa propre configuration dans un bloc de commentaires, et jusqu'à la version 0.23.15 cette configuration pouvait déclarer un serveur MCP. Il suffisait d'ouvrir le notebook en mode édition pour que marimo lance la commande de l'attaquant comme sous-processus, avant l'exécution de la moindre cellule. Le correctif dit le reste. Il retire cinq catégories entières d'environnement qu'un document téléchargé avait le droit de définir, à une priorité supérieure à la configuration de l'opérateur. Dans Bromure Agentic Coding, la liste des programmes que votre agent peut lancer est un panneau côté hôte, écrit dans la VM au démarrage, et rien dans l'espace de travail ne peut s'y ajouter.
Un dépôt peut reconfigurer votre agent de codage. Nous le savions. C'est la taille de la chose capable de le faire qui a changé. Il s'agit désormais d'un seul fichier, transféré dans Slack, que vous ouvrez pour y jeter un œil.
Quelqu'un vous envoie un notebook. C'est un notebook marimo, donc un fichier
.py ordinaire plutôt qu'un paquet de JSON avec des sorties en base64 dedans,
ce qui explique l'essentiel de l'attrait du format. Vous pouvez le lire, le
comparer, et le mettre dans git sans hook de pre-commit pour en retirer les
sorties.
Vous le lisez donc, il a l'air correct, et vous l'ouvrez en mode édition pour bidouiller une cellule.
Un sous-processus démarre. Vous n'avez pas encore exécuté de cellule ; c'est quelque chose que le fichier a demandé de lui-même, avant même que le notebook ait fini de charger.
C'est CVE-2026-75149, publiée le 19 août et reprise par The Hacker News cette semaine. CVSS 8.7, CWE-94, créditée à Gregory Tan, corrigée dans marimo 0.23.15. L'avis de VulnCheck résume le mécanisme en une phrase : le notebook contient une entrée de serveur MCP fabriquée, et l'ouvrir en mode édition lance la commande de cette entrée comme sous-processus local.
La configuration était dans un commentaire
Les notebooks marimo transportent leurs propres réglages via
PEP 723, la convention Python pour les
métadonnées de script en ligne. C'est une petite table TOML qui vit dans un bloc
de commentaires en haut du fichier, et elle existe pour qu'un script puisse
déclarer ses dépendances et sa version de Python sans fichier de projet à côté.
uv la lit. marimo aussi, qui l'étend avec une table tool.marimo pour la
configuration du notebook.
Deux propriétés de cette table ont fait les dégâts, et aucune n'est un bug en soi.
La première : marimo fusionnait les métadonnées du notebook à la priorité la plus élevée, au-dessus de la configuration définie par la personne qui exécute marimo. C'est le bon comportement par défaut pour une préférence de mise en forme. Un notebook qui veut une indentation de deux espaces doit l'emporter sur votre réglage global, parce que l'auteur sait à quoi ressemble son fichier et vous non.
La seconde : la fusion était presque sans filtre. Avant le correctif, le
nettoyeur de marimo retirait exactement une clé de la configuration fournie par
le notebook : tool.marimo.runtime.auto_instantiate. Tout le reste de
tool.marimo passait, y compris tool.marimo.mcp. C'est là que l'on liste les
serveurs Model Context Protocol, et une liste de serveurs MCP est une liste de
commandes à lancer.
Mettez les deux ensemble et un commentaire en haut d'un fichier téléchargé pouvait nommer un programme et faire en sorte que marimo le démarre. Rien ici n'est un exploit. Le fichier a utilisé la fonctionnalité telle qu'elle a été conçue, et la conception supposait que vous aviez écrit le fichier vous-même.
Le correctif est la divulgation
Le correctif, PR #10281, s'intitule « additional pep 723 sanitization », et il remplace une liste de refus par une liste d'autorisation. La configuration fournie par le notebook est désormais restreinte aux sections cosmétiques et d'éditeur : formatage, sauvegarde, affichage, raccourcis clavier, diagnostics, lint, extraits, sources de données, serveurs de langage, partage, venv, exécution, gestion des paquets.
Les sections retirées sont la moitié intéressante : ai, mcp, completion,
secrets, server.
Cela fait cinq sections entières, pas cinq clés, et un document que vous aviez
téléchargé pouvait toutes les définir. L'argumentaire de la PR détaille aussi
une seconde charge utile qui n'a jamais eu besoin de sous-processus. La section
ai contient des URL de base d'API, si bien qu'un notebook pouvait repointer le
point de terminaison du modèle vers un hôte de son choix, ce qui, selon les
mainteneurs, « pourrait permettre l'exfiltration des clés d'API de l'opérateur
vers des points de terminaison contrôlés par l'attaquant ».
Nous avons couvert la variante URL de base de cette attaque en juillet, quand
un dépôt pouvait définir ANTHROPIC_BASE_URL
et où Claude Code postait sa propre clé à l'adresse choisie par le dépôt. C'est
l'attaque la moins chère de cette catégorie, parce qu'aucun code ne s'exécute et
que c'est le client de la victime qui fait l'envoi. La variante marimo réduit le
véhicule de livraison d'un dépôt à un commentaire.
L'unité de confiance ne cesse de rétrécir
Cette classe de bugs a une histoire courte, et dans cette histoire l'attaquant a besoin de moins de votre machine à chaque fois.
En avril, Wiz Research a signalé
CVE-2026-12957
dans Amazon Q Developer, CVSS 8.5, corrigée en mai et divulguée en juin. Amazon
Q lisait .amazonq/mcp.json depuis un espace de travail ouvert et lançait les
serveurs qu'il définissait. Ces processus héritaient de tout l'environnement du
développeur : clés AWS, jetons de CLI cloud, secrets d'API, sockets d'agent SSH.
Le correctif d'Amazon a été de demander avant de lancer un serveur MCP depuis un
espace de travail non fiable.
En août, ChainDrop
commettait de la configuration d'agent dans les dépôts qu'il parcourait :
un hook SessionStart dans .claude/settings.json et une tâche folderOpen
dans .vscode/tasks.json, si bien que cloner le projet et l'ouvrir suffisait.
Ces deux-là avaient besoin d'un dépôt. Il fallait que quelqu'un publie ou compromette un projet, que vous le cloniez, et un répertoire de fichiers de configuration restait là dans l'arborescence, où une personne méfiante pouvait le regarder.
Marimo a besoin d'un fichier. Un seul fichier, qui s'affiche comme un document, et qui arrive comme arrivent les documents : un fil Slack, une pièce jointe, un gist que quelqu'un a mis en lien pendant le point quotidien, un téléchargement Kaggle. On ne clone pas un notebook. On l'ouvre, parce que l'ouvrir est la façon de découvrir ce que c'est.
Le bloc mcp s'est répandu dans la plupart de l'outillage de développement en
dix-huit mois, et ces blocs vivent dans des fichiers qui voyagent entre les
gens : racines de dépôt, répertoires d'éditeur, et maintenant métadonnées de
document. Chacun est une liste de programmes que quelque chose sur votre machine
accepte de démarrer, écrite par la dernière personne qui a touché au fichier.
Les scanners de registre ne les lisent pas, les fichiers de verrouillage ne les
couvrent pas, et personne n'a proposé d'attestation de provenance qui
s'appliquerait à un commentaire.
D'où viennent les serveurs MCP dans un profil
Bromure Agentic Coding répond à la question de la provenance là où elle peut l'être : sur l'hôte, avant le démarrage de la VM.
Chaque profil dispose d'un panneau MCP dans ses réglages, aux côtés d'Agents, Identifiants, Garde-fous et Supply Chain. Il contient une liste de serveurs. Chaque entrée s'active ou se désactive indépendamment et utilise l'un des deux transports : HTTP, une URL distante avec un jeton porteur optionnel, ou stdio, une commande locale lancée à l'intérieur de la VM. Bromure traduit cette liste dans le format attendu par l'agent actif, JSON pour Claude Code et TOML pour Codex, puis l'injecte dans la VM au démarrage.
Lisez le sens de circulation dans cette dernière phrase. La configuration va dans l'invité, depuis l'hôte, au démarrage, à partir d'une liste que vous maintenez dans un panneau de réglages. Un fichier qui apparaît ensuite dans l'espace de travail n'est pas un endroit d'où viennent les serveurs MCP : il n'y a donc pas de fusion dont la priorité pourrait être mal réglée, ni de nettoyeur à maintenir synchronisé avec un schéma de configuration qui ne cesse de gagner des sections. Un document ne peut pas ajouter une entrée à une liste qu'il ne peut pas atteindre.
Ouvrez le notebook quand même
Rien de tout cela n'aide le jour où vous exécutez une version qui a le bug. Alors prenez le marimo d'avant 0.23.15, offrez la victoire à l'attaquant, et faites passer un notebook hostile dans un profil.
Le sous-processus démarre dans l'ordinateur de quelqu'un d'autre. Le travail
de session dans Bromure Agentic Coding se passe dans des onglets kitty à
l'intérieur de la VM Ubuntu du profil, à un hyperviseur de distance de macOS.
marimo edit s'exécute là, et la commande du commentaire aussi. Sous Ressources
→ Stockage, le profil est fait de trois couches : un /home/ubuntu par profil
avec Effacer le dossier personnel…, un disque système d'espace de travail
avec Réinitialiser… qui reclone depuis l'image partagée, et en dessous un OS
de base en lecture seule auquel rien dans l'invité ne peut toucher. Une entrée
de menu emporte tout ce que l'entrée a installé.
L'environnement dont il hérite est un jeu de leurres. Le bug d'Amazon Q
faisait mal parce que le serveur lancé était un enfant du vrai shell du
développeur et héritait de ses vrais identifiants. Dans un profil Bromure, il
n'y a rien derrière ces noms de variables. Les clés d'API génériques sont des
espaces réservés brm_… exportés dans la VM et échangés contre les vraies
valeurs par le proxy de l'hôte au passage. Le kubeconfig est synthétique, avec
des certificats client jetables. Les requêtes AWS sont resignées côté hôte, si
bien qu'un processus qui contourne le proxy obtient InvalidSignatureException
plutôt qu'un accès. ~/.docker/config.json contient un faux blob base64. Les
clés SSH privées
ne sont jamais dans la VM, parce que
l'hôte signe via un agent par profil. Activez Exiger une approbation à
l'usage pour un identifiant donné et chaque échange devient une boîte de
dialogue sur l'hôte avec une autorisation limitée dans le temps : cinq minutes,
une heure, le reste de la session.
L'astuce de l'URL de base récolte un espace réservé. Le proxy ne substitue
un vrai identifiant que sur les requêtes sortantes vers l'hôte propre à cet
identifiant. Un notebook qui repointe un point de terminaison d'IA vers
attacker.example récolte ce qui traîne dans l'environnement de la VM,
c'est-à-dire une chaîne brm_… qui n'ouvre rien.
Le serveur doit atteindre le réseau. Une entrée MCP qui lance quelque chose
d'utile à un attaquant a besoin de sortie réseau, que ce soit pour récupérer une
étape ultérieure ou pour renvoyer ce qu'elle a trouvé. Garde-fous →
Connexions sortantes est une table de règles de style pf : une action, un
protocole (tcp, udp, web, any), un hôte ou un CIDR, une liste de ports,
et pour web une liste de méthodes HTTP, évaluées de haut en bas avec la
première correspondance qui gagne, plus un réglage Trafic non apparié sur
Autoriser ou Refuser. Mettez-le sur Refuser, listez les hôtes dont votre travail
a besoin, et la destination de l'entrée n'est pas dans la liste. La table est
appliquée sur l'hôte, dans le commutateur virtuel et le proxy, si bien que rien
de ce que l'invité fait à son propre routage ne change la réponse.
Encore faut-il que le fichier soit dans la VM. Un profil partage au plus
huit dossiers du Mac, chacun choisi à la main dans le panneau Dossiers et
monté sur /home/ubuntu/<basename>. Un notebook arrivé dans ~/Downloads n'est
pas dans le profil tant que vous ne l'y avez pas mis, et le reste de votre Mac
n'est pas à un parcours de répertoire de ce que le notebook a démarré.
Et vous pouvez le voir arriver. La fenêtre Journal de sécurité (Fenêtre → Journal de sécurité…) est une table chronologique unique côté hôte : verdicts sur les paquets, décisions du pare-feu, échanges d'identifiants, détections d'injection de prompt. Le code invité ne peut pas la modifier, parce que le code invité ne peut pas l'atteindre. Une connexion sortante bloquée depuis un processus que vous n'avez pas lancé y apparaît comme une ligne, ce qui vaut mieux que de l'apprendre par une facture cloud six semaines plus tard.
Activez-le
Mettez marimo à jour en 0.23.15 ou plus récent ; la liste d'autorisation qu'on y
trouve est un bon correctif. Ensuite, allez regarder tous les autres endroits
d'où votre outillage lit un bloc mcp, et demandez-vous qui a le droit d'écrire
ce fichier.
Dans un profil, les réglages qui valent deux minutes sont la même courte liste que d'habitude. Garde-fous → Connexions sortantes, avec Trafic non apparié sur Refuser et une liste d'autorisation pour les hôtes dont votre travail a besoin. Identifiants → Exiger une approbation à l'usage sur tout ce qui peut dépenser de l'argent ou supprimer des données. Supply Chain → Vérification de vulnérabilités OSV, et le filtrage socket.dev ou Delpi si vous avez une clé, par-dessus le délai de deux jours actif par défaut. Injection de prompt → le scanner de CLAUDE.md et AGENTS.md, qui traite les fichiers qui prétendent faire autorité sur votre agent exactement comme tels.
Les notebooks vont continuer d'être transférés, et les formats de configuration vont continuer de gagner des sections, parce que ces deux choses sont utiles. Ce que vous pouvez changer, c'est quelle machine écoute quand un document demande qu'un programme soit démarré. Installez Bromure Agentic Coding, gardez votre liste de serveurs dans l'éditeur de profil, et laissez le prochain bloc de commentaires serviable configurer une VM que vous pouvez effacer depuis un menu.