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

Révoquer le jeton, c'était le déclencheur

Le 8 octobre, tensorlake 0.5.144 est arrivé sur npm avec un ver Shai-Hulud à l'intérieur. Il vole les identifiants GitHub, npm, cloud et SSH, dépose un .claude/settings.json dans les dépôts de la victime, et installe un moniteur qui vérifie le jeton GitHub volé toutes les 60 secondes et lance rm -rf ~/ dès qu'il cesse de fonctionner. Révoquer le jeton, première étape du protocole de réponse à incident, efface le répertoire personnel de la victime. Dans Bromure Agentic Coding, le délai d'ancienneté par défaut cache cette version à npm, et le jeton GitHub présent dans la machine n'a jamais été le vrai.

Le ver a laissé dans son code une phrase pour qui le trouverait : « IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner ». Si vous révoquez ce jeton, l'ordinateur de son propriétaire sera effacé. Le code fait ce que la phrase annonce.

Le mercredi 8 octobre à 01 h 12 UTC, le workflow de publication de tensorlakeai/tensorlake a publié la version 0.5.144 du SDK tensorlake sur npm. Tensorlake est une plateforme de bacs à sable serverless pour agents IA, et son paquet npm est téléchargé environ 12 000 fois par semaine. La nouvelle version venait du dépôt du projet, construite par le workflow du projet, et npm affichait pour elle une attestation de provenance.

Socket l'a signalée onze minutes plus tard. StepSecurity, Aikido et The Hacker News ont suivi le même jour. La version embarque une nouvelle variante du ver Shai-Hulud, et celle-ci vient avec un otage.

Comment il est entré

StepSecurity a reconstitué la chronologie. Le 7 octobre à 01 h 20 UTC, quelqu'un a poussé un commit directement sur la branche principale du dépôt, au nom d'un mainteneur. Sept autres commits ont suivi dans les heures suivantes. Personne ne les a repérés, et le lendemain le workflow de publication a construit le code de main et l'a publié. L'attestation de provenance était exacte. Elle prouvait où le paquet avait été construit, et cet endroit était déjà entre les mains de l'attaquant.

Le package.json du paquet a gagné une ligne :

"preinstall": "node lib/setup.mjs"

npm exécute preinstall avant même que le paquet soit déballé à sa place. Le script télécharge le runtime Bun et s'en sert pour exécuter lib/Math_Symbol.js, une charge utile obfusquée de 856 Ko. Il ne fait rien en CI. Le résumé de StepSecurity : « ce sont les machines des développeurs qui sont visées ».

Ce qu'il prend, et où ça part

La charge utile collecte les jetons GitHub et npm, les clés cloud AWS, Azure et GCP, les secrets Kubernetes et HashiCorp Vault, les clés SSH, les fichiers .env, les identifiants enregistrés dans les navigateurs, les portefeuilles crypto, et la configuration des outils de code IA : .claude, .cursor, .kiro, Windsurf et Zed. Il chiffre le butin et l'envoie à iseekaigogo.com ou, à défaut, à un dépôt public qu'il crée dans le propre compte GitHub de la victime, avec la description « Shai-Hulud: Here We Go Again. » Quand le domaine tombe, il en cherche un nouveau dans un contrat Ethereum, via une trentaine de points d'accès RPC publics.

Ensuite, il se propage. Avec le jeton npm de la victime, il télécharge chacun de ses paquets, s'y ajoute, incrémente la version et republie. Avec le jeton GitHub, il commite .claude/settings.json et .vscode/tasks.json dans les dépôts de la victime, en tant que [email protected], avec le message chore: update dependencies. Le prochain développeur qui ouvre l'un de ces projets dans Claude Code ou VS Code relance le ver. Comme l'écrit Ashish Kurmi, de StepSecurity, le malware « s'exécute à nouveau quand quelqu'un ouvre le projet dans Claude Code ou VS Code ».

L'otage

La nouveauté, c'est un service appelé gh-token-monitor. Sous Linux, c'est une unité systemd utilisateur dans ~/.config/systemd/user/ ; sous macOS, un LaunchAgent dans ~/Library/LaunchAgents/com.user.gh-token-monitor.plist ; sous Windows, une tâche de connexion qui lance monitor.ps1. StepSecurity décrit ce qu'il fait :

Toutes les 60 secondes, pendant 24 heures au plus, le service vérifie ce jeton auprès de l'API GitHub. Si GitHub le rejette, le service exécute rm -rf ~/.

Mettez-vous à la place de la victime. Vous apprenez qu'un paquet que vous avez installé a volé votre jeton GitHub. Le protocole standard dit de le révoquer sur-le-champ. Vous le révoquez, et en moins d'une minute votre répertoire personnel a disparu : vos dépôts locaux, votre travail non commité, vos dotfiles, et les traces de ce que le ver a fait. Socket a trouvé la menace écrite en toutes lettres dans le code, sous forme de chaîne littérale : IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner.

Les avis de sécurité viennent donc désormais avec une règle d'ordre. L'étape 3 de StepSecurity consiste à trouver et supprimer le moniteur, et à sauvegarder d'abord tout ce que vous ne pouvez pas perdre. Son avertissement, en gras : « Ne révoquez aucun jeton GitHub avant d'avoir terminé l'étape 3. » Socket dit la même chose. Chaque heure passée à traquer le moniteur est une heure pendant laquelle l'attaquant détient un jeton valide, avec vos droits de push.

Sur un portable : la 1re étape du protocole déclenche l'effacement1. npm install[email protected]preinstall: node lib/setup.mjsVole les vraisjeton GitHub (droits push)jeton npm (publication)~/.aws ~/.kube ~/.sshInstallegh-token-monitor (LaunchAgent).claude/settings.json (dépôts)2. Le moniteur, toutes les 60 sGET api.github.com/userAuthorization: vrai jeton200 OK → il attendl'attaquant garde vosdroits de push401 → il passe à l'acte3. Vous révoquez le jetoncomme le veut le protocolerm -rf ~/dépôts locaux, travail en cours,dotfiles, et les preuves
Sur un portable, le jeton volé est le vrai, donc le révoquer est urgent. Le moniteur transforme cette révocation en effacement du répertoire personnel du développeur.

La même installation dans une machine Bromure

Dans Bromure Agentic Coding, Claude Code, Codex et les autres agents tournent dans une VM Linux sur votre Mac, une VM par machine. Chaque connexion ouverte par la VM, y compris chaque récupération npm, passe par un proxy sur le Mac. Rejouez le 8 octobre dans l'une de ces machines : l'agent ajoute tensorlake à un projet et lance npm install.

La version n'existait pas encore

Chaque nouvelle machine a le délai d'ancienneté de la chaîne d'approvisionnement activé, avec un minimum de deux jours. Quand npm demande au registre la liste des versions de tensorlake, le proxy retire toutes celles publiées il y a moins de deux jours et fait pointer latest vers la plus récente qui est assez ancienne. Le 8 octobre, c'était la 0.5.143. npm l'a installée et est passé à la suite. Depuis l'intérieur de la machine, la 0.5.144 n'a jamais existé, et le temps qu'elle dépasse le seuil, elle avait disparu de npm.

Si un lockfile ou l'agent avait demandé la 0.5.144 nommément, le proxy aurait refusé le téléchargement avec un HTTP 451 et un motif que npm affiche tel quel :

Bromure Supply-Chain Security blocked this request: npm package
[email protected] published 3 hours ago — policy requires 2 days minimum

L'agent lit cette ligne aussi, et épingle la version précédente de lui-même.

Deux autres couches se trouvent dans le même panneau Supply Chain. Avec socket.dev sélectionné et Block compromised packages activé, le téléchargement est refusé dès que socket.dev a signalé le paquet, ce qui a pris onze minutes à Socket le 8 octobre. Avec Strip preinstall / install / postinstall / prepare from npm tarballs on the fly activé, le proxy réécrit l'archive et la ligne "preinstall": "node lib/setup.mjs" disparaît avant que npm ne la voie. Cette ligne est la seule porte d'entrée du ver.

Admettons qu'il s'exécute quand même

Retirez toutes les couches et laissez la charge utile s'exécuter. Elle tourne sous l'utilisateur de la VM, dans le répertoire personnel de la machine, et part chercher ce qui figure sur sa liste.

Les jetons sont des substituts. Aucun fichier, aucune variable d'environnement, aucun processus de la VM ne contient un vrai jeton GitHub, un secret cloud ou une clé privée SSH. GH_TOKEN et ~/.git-credentials contiennent un jeton ghp_ qui a la bonne forme, et le proxy du Mac n'y substitue le vrai qu'au moment où une requête part vers GitHub. ~/.ssh ne contient aucune clé privée : SSH_AUTH_SOCK est un pont vers un ssh-agent sur le Mac, qui signe et ne cède jamais la clé. Le répertoire personnel de votre Mac, son Trousseau et ses profils de navigateur ne sont pas du tout dans la VM. Le ver chiffre et expédie un lot de substituts que GitHub, AWS et Kubernetes rejettent partout ailleurs qu'à travers ce proxy.

La sortie passe par le Mac. Une machine dont Unmatched traffic est réglé sur Deny dans son panneau Guardrails, avec des règles pour son fournisseur de modèle, ses registres et GitHub, ne se connecte jamais à iseekaigogo.com ni à aucun des 30 points d'accès RPC Ethereum. Quels que soient les réglages, chacune de ces connexions apparaît dans la Security Timeline, toujours active sur chaque machine.

Les écritures attendent un clic. Les nouvelles machines utilisent Prompt before write pour chaque forge git. Le repli GitHub du ver crée un dépôt public dans votre compte : c'est un POST /user/repos, une écriture, donc macOS affiche Allow write on "github.com" from workspace "…"? avec la requête telle qu'elle est passée sur le réseau. Sa propagation commite .claude/settings.json dans vos dépôts et les pousse : même boîte de dialogue, cette fois avec POST /acme/app.git/git-receive-pack. Vous n'avez pas demandé à l'agent de créer un dépôt nommé « Shai-Hulud: Here We Go Again. » Vous cliquez sur Don't allow, et la propagation du ver s'arrête sur votre Mac.

L'otage détient un substitut

L'unité gh-token-monitor s'installe quand même dans le ~/.config/systemd/user/ de la VM et se met à appeler api.github.com/user toutes les 60 secondes avec le jeton qu'elle a trouvé. Ce jeton est le substitut. Le proxy y substitue le vrai pour GitHub, GitHub répond 200, et le moniteur attend.

L'ordre de la réponse s'en trouve inversé. Sur un portable, le vrai jeton est entre les mains de l'attaquant, et la course consiste à le révoquer avant qu'il s'en serve, sans déclencher l'effacement. Dans Bromure, le vrai jeton n'est jamais entré dans la VM, donc l'attaquant n'a rien pour faire la course avec vous. Rien en dehors de votre proxy n'accepte la valeur ghp_ qu'il a prise.

Éteignez la machine, et le moniteur s'arrête avec elle. Ouvrez Rewind home… et choisissez le point de contrôle du démarrage précédant l'installation : Bromure en prend un à chaque démarrage réussi. Le fichier d'unité, le script dans ~/.local/bin et les fichiers .claude/settings.json déposés dans les dépôts de la machine reviennent tous à leur état d'avant. Le répertoire personnel tel qu'il était un instant plus tôt est d'abord conservé comme nouveau point de retour, si bien que le retour en arrière peut lui-même être annulé. Pour repartir de zéro, Reset disk reclone le disque système à partir de l'image de base signée.

Et si le piège se déclenche malgré tout, rm -rf ~/ supprime /home/ubuntu dans une seule VM. Le répertoire personnel de votre Mac est hors de sa portée, et Rewind home… restaure celui de la machine depuis le dernier point de contrôle.

Dans Bromure : la version est cachée, et l'otage est un substitutVM de la machine (Ubuntu)$ npm install tensorlake+ [email protected]Si la charge utile s'exécute malgré toutGH_TOKEN=ghp_… (substitut)~/.ssh : aucune clé privéeseul le home de la machinegh-token-monitor interroge GitHubavec le substitutun effacement touche /home/ubuntu de la VM,et Rewind home le restaureProxy, sur le MacDélai d'ancienneté : latest → 0.5.1430.5.144 nommément → 451, motif affichéPare-feu : iseekaigogo.com → refusPoints d'accès RPC Ethereum → refuschaque verdict dans la Security TimelineDialogue macOS : Allow write on …?POST /user/reposPOST /acme/app.git/git-receive-packVrai jeton substitué pour GitHub seulementInternetregistry.npmjs.orggithub.comfournisseur modèleC2 de l'attaquant :jamais contactéConservé sur le Mac, hors de la VMvrai jeton GitHub · clés cloud · clés privées SSH · votre dossier personnel · points de contrôle · Security Timeline
La même installation dans une machine Bromure. Le délai d'ancienneté cache la 0.5.144 à npm. Si la charge utile s'exécute malgré tout, elle vole des substituts, ses push et sa création de dépôt attendent une boîte de dialogue sur le Mac, et le moniteur otage détient un jeton que GitHub n'accepte qu'à travers le proxy de Bromure.

Qui l'a installé ?

La première question dans un incident comme celui-ci est de savoir si une machine de l'équipe a récupéré la mauvaise version. Sur un portable, on fouille les lockfiles, les caches npm et l'historique du shell, en espérant que le ver n'a pas fait le ménage derrière lui.

Dans Bromure, chaque récupération de paquet est une ligne de la Security Timeline, qu'une couche de chaîne d'approvisionnement soit activée ou non : npm · [email protected], avec la machine, l'heure et la décision. Les lignes sont conservées 90 jours sur le Mac, où rien dans une VM ne peut les modifier. Filtrez sur Supply chain, cherchez la version, et vous avez la réponse. Export… l'enregistre en CSV pour le ticket d'incident. Sur les Mac inscrits auprès de bromure.io, la même récupération part dans le flux d'événements de l'organisation sous forme d'événement supply_chain.fetch, si bien qu'une équipe de sécurité peut poser la question une seule fois pour tous les développeurs.

La mauvaise version n'est jamais apparue

Le délai d'ancienneté, actif par défaut avec un minimum de deux jours, retire les versions trop récentes de la liste des versions de npm. Le 8 octobre, npm install tensorlake a résolu vers la 0.5.143.

Le jeton volé était un substitut

Les identifiants GitHub, npm, cloud et SSH restent sur le Mac. L'otage du ver était une valeur ghp_ qui ne fonctionne qu'à travers le proxy de Bromure.

Se propager exige votre clic

Créer un dépôt et pousser un commit sont des écritures. Avec Prompt before write, le réglage par défaut des nouvelles machines, les deux attendent une boîte de dialogue macOS à laquelle la VM ne peut pas répondre.

L'effacement touche une seule VM

rm -rf ~/ atteint le répertoire personnel de la machine, pas le vôtre. Rewind home le restaure depuis le point de contrôle pris au dernier démarrage.

Le prochain protocole

Shai-Hulud revient sans cesse cette année, et ses auteurs façonnent chaque version en fonction de la façon dont les défenseurs ont géré la précédente. Les défenseurs révoquaient les jetons en quelques heures, alors celle-ci punit la révocation. La prochaine punira autre chose : lancer un script de nettoyage, supprimer un LaunchAgent, rouvrir le projet.

Sur un portable, chacun de ces retournements fonctionne parce que l'attaquant détient quelque chose de réel sur la machine même que vous essayez de nettoyer. Dans une machine Bromure, les vrais identifiants, votre répertoire personnel et la trace de ce qui s'est passé se trouvent tous sur le Mac, derrière le proxy. Le nettoyage est un retour en arrière, et vous pouvez le faire avant de changer le moindre identifiant. Pour en savoir plus sur la frontière réseau qui rend cela possible, lisez L'installation était propre et Quarante mille dollars par évasion.