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

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.
Une URL de connexion, deux relais vers l’hôteauthorization_endpoint: https://evil.test/$([IO.File]::WriteAllText(…))opencode 1.18.32 dans WSLInvité Linuxopencode MCP OAuth[email protected]Hôte Windowspowershell -EncodedCommandStart "…" "https://…$(…)"$(…) se développe d’abordla commande tourne sous votre comptelancement raté, erreur ignoréeL’URL devient une partie d’une ligne de commande sur l’hôte.Un agent dans une machine BromureVM Linuxxdg-open, $BROWSER→ bromure-openFichier de sortiel’URL sur une ligne de texteaucun shell ne la litVotre Machttp ou https, hôte public seulementNSWorkspace.open(url)L’URL reste une URL de la VM au navigateur. $(…) reste du texte brut.
La même URL de connexion sur deux machines. Dans WSL, l’ouvreur de l’invité Linux passe l’URL à powershell.exe sur l’hôte Windows, qui développe la sous-expression $(...) et l’exécute avec les droits de votre compte Windows. Dans Bromure Agentic Coding, l’ouvreur de l’invité écrit l’URL dans un fichier destiné au Mac. Le Mac ne l’accepte que comme adresse http ou https vers un hôte public et la passe au lanceur d’URL de macOS, qui ne démarre aucun shell.

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.

MCP distant dans Bromure Agentic CodingVM LinuxConfig MCP de l’agentURL du serveur, écrite au bootni jeton, ni connexion à faireClés et jetonsdoublures seulementxdg-open → bromure-openles URL sortent en texteVotre MacCourtier OAuthdécouverte, connexion PKCE, jetongardé sur le MacProxyrépond à la découverte de l’agentajoute le vrai jeton sur le réseauallow web mcp.linear.appallow web mcp.notion.comallow web api.anthropic.comdefault denymcp.linear.applisté, joint avec le vrai jetonmcp-linear[.]appnon listé : refusé au commutateurligne deny dans la Security Timeline
Un serveur MCP ajouté dans Bromure. Le courtier OAuth du Mac se connecte et garde le jeton. Le proxy répond aux requêtes de découverte de l’agent, si bien que l’agent ne lance jamais sa propre poignée de main et n’ouvre jamais une URL fournie par le serveur. La liste d’autorisation sur le Mac limite les hôtes MCP que la VM peut joindre, et chaque refus est journalisé.

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.