Retour à tous les articles
Publié le · par Renaud Deraison

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.

analysis.py · un fichier, transféré jusqu'à vous# /// script# dependencies = ["polars", "altair"]# [tool.marimo.mcp.servers.helper]# command = "…"# ///Métadonnées PEP 723 · une table TOML en commentaireimport marimo as mo@app.celldef _(): …les cellules · ce dont on vous a dit de vous méfierfusion de configuration, avant 0.23.15votre config · priorité la plus bassela config du fichier · priorité la plus hauteune seule clé retirée : runtime.auto_instantiatemarimo edit analysis.pyentrée de serveur MCP → sous-processus localavant l'exécution de la moindre cellulehérite du shell depuis lequel il a été ouvertL'habitude que le format vous a appriseOuvrir le notebook, lire les cellules, décider quoi exécuter. Inspection d'abord, exécution ensuite, ce qui esttoute la raison d'être du format. L'entrée MCP s'exécutait pendant l'étape un.
D'où venait la commande. Un notebook marimo est un unique fichier .py dont les réglages vivent dans un bloc de commentaires PEP 723 en haut. Avant 0.23.15, marimo fusionnait ce bloc par-dessus la configuration de l'opérateur en ne retirant qu'une seule clé, si bien qu'une section mcp dans le commentaire devenait un programme que marimo lançait à l'ouverture, en mode édition, avant l'exécution de la moindre cellule.

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.

La config voyage avec le fichierqui écrit la liste des serveurs ?celui qui a modifié le fichier en dernierquand est-elle lue ?à l'ouverture, avant toute décisionqu'est-ce qui l'emporte ?le téléchargement bat votre propre configde quoi hérite le sous-processus ?du shell d'où vous l'avez ouvert, et avec lui~/.aws · ~/.ssh · SSH_AUTH_SOCK · clés d'APILa config vit dans le profilqui écrit la liste des serveurs ?vous, dans le panneau MCP du profil, sur l'hôtequand est-elle lue ?au démarrage, traduite au format de l'agentqu'est-ce qui l'emporte ?rien de l'espace de travail n'est dans la fusionde quoi hérite le sous-processus ?de l'environnement d'une VM jetable : clés leurres,pas de clé SSH privée, sortie filtrée côté hôte
Deux réponses à la même question : qui a le droit d'ajouter un programme à la liste des choses que votre outillage va lancer ? À gauche, la dernière personne à avoir modifié le fichier que vous avez ouvert, fusionné par-dessus votre propre configuration à une priorité supérieure, sur une machine qui détient vos identifiants cloud. À droite, vous, dans un panneau de réglages sur l'hôte, écrit dans une VM jetable au démarrage, où les identifiants proposés sont des leurres et où une table que l'invité ne peut pas modifier filtre le trafic sortant.

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.