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

Une session, cent dépôts

Le rapport AI Risk and Resilience 2026 de Mandiant décrit un attaquant qui a pris le contrôle d'une session d'assistant de code en cours chez un fournisseur SaaS, y a fait installer un paquet PyPI piégé, volé des jetons OAuth GitHub et propagé le ver Shai-Hulud dans une centaine de dépôts internes. Le rapport ne dit pas comment la session a été prise. Dans un workspace Bromure Agentic Coding, il n'a pas besoin de le dire : le jeton présent dans cette session est un leurre `ghp_`, la tentative de l'exfiltrer met la VM en pause, et les push qui transforment une machine en cent dépôts s'arrêtent au proxy de l'hôte.

Quelqu'un a pris le contrôle d'une session de code qui tournait, authentifiée et de confiance, sur la machine même d'un développeur. Tout ce qui a suivi, jusqu'à une centaine de dépôts, est sorti de ce que cette machine détenait à cet instant.

Un développeur d'une entreprise de logiciel en tant que service a demandé à son assistant de code quel paquet utiliser pour un problème ordinaire. L'assistant en a nommé un. Le développeur a dit oui, ce qui est tout l'intérêt d'avoir un assistant, et pip install a fait le reste.

Un attaquant tenait déjà la session, et c'est lui qui a tapé cette recommandation.

Mandiant décrit le cas dans son AI Risk and Resilience Report 2026, publié ce mois-ci par la branche réponse à incident de Google Cloud ; The Hacker News l'a relayé le 16 septembre. L'attaquant a détourné une session d'assistant de code active sur le poste d'un développeur. Cinq étapes de plus l'ont mené à une centaine de dépôts de code internes et à l'espace de noms de paquets de l'entreprise, où un second employé a récupéré la version empoisonnée.

La chaîne, maillon par maillon

Un poste, six étapesce que l'étape dépense1un attaquant prend une session d'assistant activeméthode non divulguée dans l'étude de cas publiquela session2l'assistant recommande un logiciel piégéle développeur l'accepte, comme prévule oui du développeur3un paquet PyPI compromis installe un voleur de donnéestrafic de dépendances ordinaire, installation ordinaireconfiance du registre4les jetons OAuth GitHub sont lus sur le postele pivot : tout ce qui suit en dépendjetons stockés5Shai-Hulud se propage à une centaine de dépôts internessecrets des dépôts et code source produit emportésaccès en écriture6un paquet de l'espace de noms de l'entreprise est piégéun second employé récupère cette version et est infectédroits de publication
Les six étapes décrites par Mandiant, et l'autorisation que chacune dépense. Mandiant ne divulgue aucune méthode pour l'étape 1. À chaque étape suivante, l'attaquant se sert d'une autorisation que la machine du développeur détenait déjà : la confiance de la session, puis celle du gestionnaire de paquets, puis celle de GitHub, puis les droits de publication de l'entreprise.

Mandiant retient précisément ce que vous voudriez le plus savoir. L'étude de cas publique ne dit pas quand l'intrusion a eu lieu, et elle ne dit pas comment l'attaquant a pris le contrôle d'une session en cours. Cookies de navigateur volés, extension malveillante, CLI compromise sur une machine partagée : le rapport n'en retient aucun. Qui le lit pour trouver le correctif repartira les mains vides.

Les étapes 2 à 6 ne contiennent aucun exploit. Une installation de paquet fonctionne comme fonctionnent les installations de paquets, et du code exécuté sous l'identité du développeur lit les jetons posés sur le disque du développeur. Un git push muni d'un jeton valide pousse, et une publication munie de droits de publication valides publie. Une étape était une intrusion ; les cinq autres étaient de la dépense.

Le multiplicateur, ce sont les identifiants, et il ne cesse de grandir

Shai-Hulud est le ver auto-répliquant de la chaîne d'approvisionnement qui dévore des comptes de mainteneurs depuis 2025, et il apparaît comme charge utile dans une histoire pareille parce que l'étape 5 est exactement ce pour quoi ses auteurs l'ont construit. GitGuardian a disséqué une variante récente en août et l'a trouvée à ratisser 469 emplacements d'identifiants, contre 189 dans les versions antérieures : environnements de développement, outillage CI/CD, configuration cloud, configurations d'outils d'IA, fichiers de configuration des gestionnaires de paquets, historique du shell, fichiers .env, réglages d'IDE, caches de CLI. Le ver lit un classeur plutôt qu'il ne cherche une faille.

L'analyse de GitGuardian résume la catégorie en une phrase :

« Les attaquants ont cessé d'essayer de briser les relations de confiance et se sont mis à utiliser les identifiants qui font déjà fonctionner ces relations. »

Un poste de travail est devenu cent dépôts parce qu'il détenait de quoi autoriser cent dépôts. Ce nombre est sorti d'un inventaire, et c'est vous qui décidez de ce qui entre dans l'inventaire.

La session ne détenait que des leurres

Bromure Agentic Coding exécute l'agent dans une machine virtuelle Linux sur votre Mac, et cette machine ne détient aucun secret réel. Vous n'avez rien à activer pour cela. C'est la façon dont un workspace stocke un identifiant, tout simplement.

Votre vrai jeton GitHub reste chiffré sur l'hôte. La VM reçoit un faux qui préserve la structure : ghp_ suivi de 36 caractères, 40 en tout, si bien que les contrôles de préfixe et de longueur de gh passent sans broncher. Bromure l'exporte comme GH_TOKEN et l'écrit dans ~/.git-credentials et dans la configuration de gh ; c'est le seul identifiant GitHub présent dans cette machine. Un proxy sur l'hôte substitue la vraie valeur sur le fil après que la requête a quitté la VM, et seulement quand la destination correspond à l'hôte pour lequel l'identifiant a été forgé. Chaque faux est déterministe, dérivé de la vraie valeur et d'un sel de 32 octets propre à l'installation via HKDF-SHA256, de sorte qu'un client qui prend l'empreinte de sa propre clé ne voit aucune rotation apparente d'une session à l'autre.

Passez l'étape 4 là-dessus. Le voleur de données arrive, ratisse les 469 emplacements, trouve la variable d'environnement, trouve ~/.git-credentials, trouve la configuration de gh, et expédie sa récolte. Chaque lecture réussit, et chaque fichier se trouve là où le ver l'attendait. L'attaquant se retrouve avec une chaîne de quarante caractères qui n'authentifie nulle part, sauf à repasser par un Mac bien précis.

Les étapes 5 et 6 ont toutes deux besoin que l'étape 4 ait produit un jeton fonctionnel, et l'étape 4 a produit un leurre.

Le vol est aussi l'alarme

Un faux jeton n'a qu'une destination légitime. Aucune requête innocente ne transporte un leurre ghp_ forgé pour github.com vers un autre hôte ; le proxy inspecte donc chaque requête sortante, en-têtes et corps, à la recherche d'un faux qui quitterait son périmètre. Il fait tourner un automate d'Aho-Corasick, assez peu coûteux pour être braqué sur l'ensemble du trafic.

Quand le proxy en trouve un, il fait plus que journaliser :

  • il refuse la requête avec un HTTP 451, et pas un octet n'atteint la destination ;
  • Bromure met la VM en pause sur-le-champ ;
  • une alerte vous propose Éteindre, Conserver pour investigation (exporter d'abord le disque, le dossier personnel et les dossiers partagés pour l'analyse forensique) ou Continuer à vos risques et périls ;
  • la Security Timeline gagne une ligne rouge Credential brokering ;
  • Bromure marque le workspace comme compromis, de sorte que le prochain lancement efface l'image disque et le dossier personnel persistant. Vos jetons, vos clés SSH et vos réglages de workspace survivent à cet effacement.

Vous n'activez rien de tout cela ; le détecteur est toujours en service. C'est la tentative d'exfiltration de l'étape 4 qui arrête la session, et l'incident se termine donc sur une VM en pause et une entrée de timeline plutôt que sur une centaine de dépôts et une étude de cas.

Cent dépôts, cela fait cent push

Supposons que vous vouliez la ceinture en plus des bretelles. L'étape 5, c'est un ver qui écrit dans des dépôts, et l'hôte classifie les écritures.

Les garde-fous d'un workspace portent une politique d'écriture par service. Pour GitHub, cette politique lit le git-over-HTTPS aussi bien que l'API REST : un git push arrive en git-receive-pack et compte comme une écriture, tandis qu'un git fetch arrive en git-upload-pack et compte comme une lecture. Lecture seule refuse les push. Demander avant d'écrire, le réglage par défaut des nouveaux workspaces, retient chacun d'eux pour une boîte de dialogue côté hôte qui nomme l'opération. Les fetch passent dans les deux cas, donc l'agent continue de travailler.

Le proxy tranche sur l'hôte, à l'extérieur de la VM. Un agent dont la session appartient à quelqu'un d'autre ne peut ni désactiver la politique ni la contourner, parce que la politique ne s'exécute pas du côté de l'agent. Un ver qui veut cent dépôts récolte cent refus, ou vous pose cent fois la question.

Et le paquet devait encore arriver

L'étape 3 est l'endroit où Mandiant donne un conseil concret : valider les dépendances tierces recommandées par l'IA contre des sommes de contrôle cryptographiques et des listes d'autorisation, et faire transiter le trafic de dépendances par des dépôts que vous contrôlez.

Dans un workspace, chaque récupération de paquet passe déjà par l'hôte, puisque le proxy est la seule route de la VM vers le réseau. Bromure intercepte PyPI sur pypi.org et files.pythonhosted.org et y applique la politique de l'hôte. Le pip.conf présent dans la VM peut resserrer ce que le proxy a servi ; il ne peut pas le desserrer. Trois couches ont voix au chapitre pour les paquets Python :

  • la barrière d'âge, active par défaut avec un minimum de deux jours, qui refuse les versions plus jeunes que le seuil, au motif qu'une publication toute fraîche est celle qui a le plus de chances de venir d'être détournée. Les index PEP 503 ne portent pas d'horodatage, alors Bromure va chercher la date de publication à la demande ;
  • le contrôle OSV auprès d'api.osv.dev, gratuit et sans clé, qui bloque toute version portant un avis de sécurité au niveau de gravité que vous choisissez ou au-dessus ;
  • le filtre de paquets compromis de socket.dev, qui se déclenche sur les malwares, les malwares connus, le typosquatting et les scripts d'installation malveillants.

Un blocage revient sous forme d'un HTTP 451 dont le corps commence par Bromure Supply-Chain Security blocked this request: suivi du motif, et pip l'affiche mot pour mot. Vous voyez pourquoi l'installation a échoué, et l'agent aussi, qui sait souvent épingler tout seul une version plus ancienne. Cette version plus ancienne est précisément celle vers laquelle la barrière d'âge le poussait.

Un poste ordinaireétape 3 : le paquet s'installele registre sert la version piégéerien sur le trajet ne juge la provenanceétape 4 : les jetons sont lusvrais jetons OAuth : env, config, historiquel'exfiltration livre un identifiant valideétape 5 : le ver pousseun jeton valide pousse, donc il pousseenviron 100 dépôts, plus l'espace de nomsDans un workspace Bromureétape 3 : la requête est jugéeâge, OSV, socket.dev, sur l'hôte451 avant qu'un octet n'atteigne la VMétape 4 : la lecture réussit aussiles 469 emplacements tiennent des leurresen envoyer un : 451, et la VM se met en pauseétape 5 : le push est classifiégit-receive-pack refusé ou soumis à accordgit fetch passe toujours ; le travail continue
Les trois mêmes étapes porteuses, sur deux machines. À gauche, un poste ordinaire : de vrais jetons sur le disque, un vol qui réussit et des push qui passent. À droite, un workspace Bromure Agentic Coding : l'hôte refuse la version piégée avant qu'elle n'atteigne la VM, le même vol rapporte un leurre et met la machine en pause, et l'hôte classifie les push, là où une session détournée n'a pas voix au chapitre.

Les trois contrôles de Mandiant, et où ils habitent

Le rapport clôt l'étude de cas sur trois recommandations. Chacune nomme un endroit où un workspace Bromure place déjà la frontière.

Valider les dépendances recommandées par l'IA

Mandiant réclame des sommes de contrôle et des listes d'autorisation sur tout ce que l'assistant suggère. Votre workspace applique sa politique à la réponse du registre avant que la VM ne la voie. Le proxy retire du listing de versions celles qui sont trop fraîches, si bien que du point de vue de l'agent elles n'existent pas encore, et il refuse une version trop fraîche épinglée en citant dans l'erreur l'âge réel du paquet. Les clés des services de réputation restent sur l'hôte, en dehors de la machine qui exécute l'installation.

Tenir les clés brutes et les jetons durables à l'écart

C'est la frontière du fil, écrite sous forme de conseil. Aucun fichier, aucune variable d'environnement, aucun processus dans la VM ne détient de vraie clé d'API, de jeton OAuth, de secret AWS ou de clé privée SSH. Bromure garde les jetons OAuth d'abonnement chiffrés sur l'hôte et les renouvelle là-bas environ cinq minutes avant l'expiration, de sorte que l'invité ne porte aucun jeton de rafraîchissement. Le canal entre les deux ne va que dans un sens : l'hôte y écrit des faux, et la VM ne dispose d'aucun appel pour redemander un vrai jeton.

Faire passer le trafic de dépendances par ce que vous contrôlez

Le proxy est la seule route de la VM vers l'extérieur. Chaque requête HTTPS voyage par un socket virtuel jusqu'à l'hôte, qui termine le TLS, inspecte la requête et la réémet à travers la pile TLS de macOS. C'est ce chemin unique qui rend les deux autres contrôles exécutoires plutôt que consultatifs. Aucune récupération de paquet ne peut sauter les vérifications, et aucun identifiant ne peut partir sans être substitué.

Et réduire le rayon d'explosion, délibérément

Cent dépôts sont tombés parce qu'une machine portait de quoi autoriser cent dépôts. Découpez les workspaces un par projet, ou un par frontière d'identifiants. Chacun correspond à une seule VM, deux workspaces n'en partagent jamais une, et chacun porte les identifiants dont le travail de ce workspace a besoin. L'inventaire d'une machine donnée se règle dans un panneau de préférences.

La partie que vous ne pouvez pas corriger

Mandiant cadre la catégorie en une phrase qui décrit la forme plutôt que l'incident : « une source de données empoisonnée, une dépendance de modèle ou un point d'accroche d'extension peut transformer un agent de confiance en canal de reconnaissance interne, de déplacement latéral ou d'évasion autonome hors d'un bac à sable. »

Trois points d'entrée différents, une seule phrase, et elle vaut pour les trois. Le point d'entrée détermine la façon dont un attaquant arrive ; le contenu de la machine d'un développeur détermine jusqu'où il va. Cet attaquant-ci est entré par une session détournée. Le suivant entrera par autre chose, et le rapport ne nomme aucune porte que vous puissiez verrouiller.

Construisez pour l'étape d'après l'entrée. Supposez que la session appartient déjà à quelqu'un d'autre, ce que l'étude de cas vous impose de supposer en n'expliquant jamais l'étape 1, puis demandez-vous ce que cette session peut atteindre.

Une version de cet incident s'arrête à l'étape 3, sur un 451 dans le terminal et une ligne dans le Security Log. Une autre s'arrête à l'étape 4, sur une VM en pause et une ligne rouge dans la timeline consignant qu'un identifiant a tenté de sortir. Aucune des deux n'atteint l'étape 5, et c'est à l'étape 5 que se trouvaient les cent dépôts.

Mettez l'agent sur une machine qui ne détient que des leurres. Installez Bromure Agentic Coding, et laissez quelqu'un prendre la session.