Le lien de connexion était une commande
Le 19 octobre, Pillar Security a révélé une faille d’exécution de code à distance dans opencode sous Windows et WSL. Un serveur MCP distant pouvait glisser une sous-expression PowerShell dans une URL de connexion https:// ordinaire, et l’ouvreur de navigateur d’opencode l’exécutait pendant la poignée de main OAuth. Le correctif est sorti sous forme de refactorisation, sans CVE ni avis de sécurité. Dans Bromure Agentic Coding, c’est le Mac qui mène lui-même les connexions MCP, et une URL venue de la VM arrive sur le Mac sous forme de texte qu’aucun shell ne lit jamais.
Le serveur a choisi la page de connexion, et l’adresse de cette page contenait une commande. L’agent l’a ouverte, et PowerShell a exécuté la commande avant qu’un navigateur n’apparaisse.
Vous ajoutez un serveur MCP distant à votre agent de code. MCP, le Model Context Protocol, est la façon dont les agents atteignent des outils extérieurs : un gestionnaire de tickets, une base de données, un index de documentation. Le serveur vous demande de vous connecter, alors l’agent lance la poignée de main OAuth. Il demande au serveur où se trouve sa page de connexion, reçoit une adresse et l’ouvre dans votre navigateur. Vous ne touchez jamais à cette adresse. Vous épargner ce copier-coller, c’est tout l’intérêt de la poignée de main.
Le 19 octobre, Pillar Security a publié Open and Shut: Remote Code Execution in opencode via MCP OAuth, signé Eilon Cohen. opencode est un agent de code open source qui compte environ 3 millions de téléchargements npm par semaine. Sous Windows et dans WSL, les versions 1.0.137 à 1.18.32 exécutaient pendant cette poignée de main une commande choisie par le serveur MCP, sans invite et sans clic. Pillar propose un score CVSS de 9,8. Infosecurity Magazine en a parlé le 20 octobre.
D’un document de découverte à PowerShell
C’est le serveur qui contrôle l’adresse. Dans la poignée de main OAuth, le
document de découverte du serveur indique un authorization_endpoint, une
chaîne libre, et le client vous y envoie. Pillar a suivi son trajet dans
opencode 1.18.32 :
server's discovery doc → MCP SDK → oauth-provider.ts → mcp/index.ts → browser.ts → open() → PowerShell
Rien sur ce trajet ne vérifiait le schéma ni ne comparait l’adresse au
serveur que vous aviez configuré. open est un paquet npm populaire qui
lance un navigateur, avec environ 184 millions de téléchargements par
semaine. Sous macOS et Linux, il passe l’URL à open ou xdg-open comme
un seul argument, donc aucun shell ne la lit. Sous Windows, et dans WSL, où
il sollicite l’hôte Windows, la version 10.1.2 construit une ligne de
commande PowerShell. Elle échappe le nom de l’application et laisse l’URL
telle quelle, entre guillemets doubles. PowerShell développe $(...) dans
les chaînes entre guillemets doubles. La charge utile de Pillar :
https://evil.test/$([IO.File]::WriteAllText('C:\Users\Public\pwned.txt','RCE'))
PowerShell évalue la sous-expression avant même d’essayer de lancer quoi que ce soit. Quand le lancement du navigateur échoue, le fichier est déjà sur le disque, et opencode ignore l’erreur.
L’URL commence par https://. Après un bug identique dans mcp-remote
(CVE-2025-6514, juillet
2025), le conseil habituel était de n’accepter que les liens http: et
https:. Le SDK TypeScript de MCP bloque javascript:, data: et
vbscript:. Le propre correctif d’opencode a ajouté une vérification
http/https. La charge utile de Pillar passe les trois. Ce qui a fermé la
brèche dans opencode 1.18.33, c’est la mise à jour vers open 11.0.4, qui
entoure l’URL de guillemets simples, où PowerShell ne développe rien.
Deux autres détails rendent celle-ci difficile à gérer :
- Le serveur peut devenir hostile après que vous l’avez ajouté. La charge utile arrive dans une réponse HTTP au moment de la connexion. Un serveur sain le jour où vous l’avez configuré peut être compromis plus tard, tout comme son domaine, son entrée dans un registre ou une copie typosquattée.
- Aucun scanner ne la signalera. Pillar a signalé le bug via un avis GitHub privé le 29 août. Anomaly, l’éditeur d’opencode, a répondu que les serveurs MCP malveillants sortaient de son modèle de menace, a fusionné le correctif le 25 septembre comme une refactorisation du code de lancement du navigateur, et a publié la 1.18.33 le 28 septembre. Il n’a demandé aucune CVE et publié aucun avis, donc les robots de dépendances n’ont rien à comparer. Pillar conseille d’inventorier à la main les installations d’opencode.
WSL et Bromure, à un choix de conception près
WSL transforme ce bug en compromission de l’hôte. Il fait tourner Linux par-dessus Windows, et quand un programme à l’intérieur veut un navigateur, la demande part vers l’hôte. C’est un shell Windows qui fait l’ouverture, donc une commande cachée dans l’URL s’exécute sur votre machine Windows, avec votre compte Windows, hors de Linux.
Bromure Agentic Coding a la même disposition. Il fait tourner Claude Code, Codex, Grok, Kimi et omp dans des VM Linux sur votre Mac, et une page de connexion ouverte depuis la VM s’affiche dans le navigateur de votre Mac. Les deux divergent au moment du passage de relais.
Dans chaque machine Bromure, xdg-open et $BROWSER pointent vers un
petit script appelé bromure-open. Le paquet open sous Linux appelle
xdg-open, comme la plupart des CLI qui « ouvrent un navigateur ».
bromure-open fait une seule chose : il écrit l’URL, sur une seule ligne
de texte, dans un fichier d’un dossier partagé avec le Mac. Il n’y exécute
ni n’y développe rien.
Le Mac récupère le fichier, le supprime et lit la ligne comme une URL.
Seules les adresses http et https vers un hôte public passent.
mailto:, file:, les schémas personnalisés, localhost et les adresses
privées sont écartés. Ce qui reste va à NSWorkspace.open, l’appel macOS
qui transmet une URL à votre navigateur par défaut. Aucun shell
n’intervient, d’un côté comme de l’autre de la frontière de la VM.
L’URL de Pillar atteint votre navigateur en tant qu’URL, avec
$([IO.File]::WriteAllText(…)) posé dans son chemin comme du simple
texte. Le navigateur ouvre une page sur evil.test, ou n’y arrive pas, et
aucune commande ne s’exécute.
C’est le Mac qui fait la connexion MCP
Avec les agents que fournit Bromure, l’URL de connexion n’atteint même pas la VM. Vous ajoutez les serveurs MCP distants une fois, dans le panneau MCP de la machine. Bromure traduit chacun dans le format de configuration de Claude Code, Codex, Grok, Kimi et omp et le remet à la VM au démarrage.
Pour un serveur HTTP, Authorize with OAuth… mène la connexion sur le
Mac. Le courtier de Bromure récupère le document de découverte du serveur,
s’enregistre sous le nom Bromure AC, construit l’URL d’autorisation avec
les API d’URL d’Apple et l’ouvre avec NSWorkspace.open, puis intercepte
le rappel sur un port de bouclage du Mac. Le jeton reste sur le Mac. La
configuration de l’agent ne contient aucun identifiant, et le proxy ajoute
l’en-tête d’autorisation aux requêtes destinées à ce serveur.
Le proxy répond aussi lui-même aux requêtes de découverte OAuth adressées
au serveur. L’agent voit un serveur auquel il est déjà connecté, donc il ne
lance jamais sa propre poignée de main, ne demande jamais au serveur un
authorization_endpoint et n’en passe jamais à un ouvreur. Le chemin de
code exploité par Pillar ne s’exécute pas.
La VM ne décide pas où elle se connecte
Pillar prévient qu’un serveur peut devenir hostile après que vous l’avez ajouté. Bromure contrôle, depuis le Mac, les serveurs que la VM peut joindre. Chaque connexion d’une machine passe par un commutateur réseau virtuel et un proxy, tous deux hors de la VM. Dans le panneau Guardrails de la machine, réglez Unmatched traffic sur Deny et listez les serveurs MCP que vous utilisez :
allow web mcp.linear.app
allow web mcp.notion.com
allow web registry.npmjs.org
allow web github.com
allow web api.anthropic.com
default deny
Un serveur absent de la liste, comme un typosquat ou une entrée copiée
depuis un forum, ne peut pas servir de document de découverte, donc il n’y
a aucun authorization_endpoint à ouvrir. Le commutateur vérifie chaque
connexion par IP et par le nom d’hôte vu dans le DNS, et le proxy la
revérifie par le nom de serveur TLS. Chaque refus est une ligne rouge
deny dans la Security Timeline (Window → Security Timeline…),
avec l’hôte, le port et la machine. Les règles s’appliquent dès que vous
cliquez sur Save, et rien dans la VM ne peut les modifier.
Dans la VM, chaque clé est une doublure. ANTHROPIC_API_KEY, le jeton
gh, le secret AWS et les jetons MCP statiques contiennent de fausses
valeurs, et le proxy n’y substitue la vraie que sur une requête vers l’hôte
pour lequel elle a été émise. Si une commande collecte ces valeurs et en
envoie une vers un hôte auquel elle n’appartient pas, le détecteur de
compromission répond HTTP 451, ne transmet rien et met la machine en pause
avec This environment may have been compromised.
Trouver la version qu’aucun scanner ne signale
Faute de CVE, Pillar conseille d’inventorier vous-même les installations d’opencode. Dans Bromure, les relevés existent déjà.
Trouver la version. La Security Timeline enregistre chaque
téléchargement de paquet, quels que soient les réglages de chaîne
d’approvisionnement, avec le paquet et la version dans la colonne
Condition. Tapez opencode-ai dans Filter events… : toute ligne
indiquant npm · [email protected] ou plus ancienne désigne la machine.
Export… enregistre les lignes en CSV. Sur les Mac inscrits auprès de
bromure.io, chaque téléchargement est aussi un événement
supply_chain.fetch, si bien qu’un administrateur peut chercher
opencode-ai sur les machines de tous les développeurs depuis une seule
console au lieu de sonder chaque équipe.
Trouver les serveurs. bromure-cli trace hostnames liste chaque hôte
qu’une machine a contacté, avec le nombre de requêtes. Les hôtes MCP
auxquels parlent vos agents figurent dans cette liste, y compris ceux qui
ne sont jamais passés par le panneau MCP. Ce sont eux qu’il faut
examiner, ou laisser hors de la liste d’autorisation.
Mettre à jour dans la VM. npm install -g opencode-ai@latest dans le
terminal de la machine installe la 1.18.33 ou plus récente, et le nouveau
téléchargement apparaît dans la Timeline comme preuve.
Les URL quittent la VM sous forme de texte
xdg-open et $BROWSER écrivent l’URL dans un fichier destiné au Mac.
Le Mac n’accepte que http ou https vers un hôte public et l’ouvre sans
shell.
La connexion MCP sur le Mac
Le courtier mène OAuth et garde le jeton. Le proxy répond à la découverte de l’agent, donc l’agent n’ouvre jamais une URL de connexion fournie par le serveur.
Une liste d’autorisation pour les hôtes MCP
Avec Unmatched traffic réglé sur Deny, seuls les serveurs listés sont joignables. Chaque refus est une ligne dans la Security Timeline.
Un inventaire sans CVE
Chaque téléchargement de paquet est enregistré avec sa version, et chaque hôte contacté par une machine est à une commande de distance.
Chaque champ est non fiable
La leçon finale de Pillar s’adresse à ceux qui écrivent des clients MCP : chaque champ envoyé par un serveur est une entrée non fiable, et l’essentiel est analysé bien avant que le modèle ne lise quoi que ce soit. Dans ce bug, le modèle n’a jamais vu la charge utile : c’est un lanceur de navigateur qui l’a exécutée.
Vous ne pouvez pas auditer chaque lanceur de chaque agent que vous installez, et celui-ci a été corrigé sans que personne ne soit prévenu. Bromure sort les décisions de la VM. Le Mac mène les connexions, ouvre les URL et décide quels hôtes la VM peut joindre. C’est aussi le Mac qui détient les vrais identifiants. Pour un cas antérieur où le serveur MCP lui-même était l’attaquant, lisez Le modèle, c’était leur serveur. Pour une configuration d’agent qui faisait confiance à la mauvaise source, lisez Vous faisiez confiance au workspace, pas aux serveurs.