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

Le ver est passé open source

Au cours de la semaine du 11 mai 2026, les auteurs de Shai-Hulud — le ver auto-réplicatif de la chaîne d'approvisionnement npm qui dévore les comptes de mainteneurs depuis septembre 2025 — ont eux-mêmes divulgué leur code source. Dès le week-end, OX Security avait identifié quatre paquets npm typosquattés publiés depuis un même compte ; l'un est une copie quasi mot pour mot du ver divulgué, un autre un bot DDoS écrit en Golang, et les deux derniers de simples infostealers qui expédient des clés SSH et des portefeuilles crypto vers des C2 bas de gamme. Le seuil d'entrée pour forker une attaque de la chaîne d'approvisionnement vient de chuter sévèrement, et les premiers à installer ces paquets ne sont plus des humains.

La semaine dernière, les auteurs de Shai-Hulud — le ver npm qui ronge les comptes de mainteneurs depuis septembre 2025 — ont divulgué leur propre code source. Dès le week-end, un compte npm nommé deadcode09284814 avait publié quatre typosquats réutilisant ce code. L'un était le ver, quasi à l'identique. Un autre, un bot DDoS écrit en Golang. Deux étaient de simples infostealers qui envoient en POST vos clés SSH, votre ~/.aws/credentials et votre coffre MetaMask vers une IP louée. Deux mille six cent soixante-dix-huit installations plus tard, la question n'est plus « est-ce que quelqu'un va armer la fuite ». La question est de savoir lequel des agents qui tapent npm install dans votre terminal cet après-midi en attrapera un.

Il existe un phénomène qui se produit, périodiquement, dans le logiciel offensif : un outil fermé est balancé sur un forum, et la population de gens capables de le faire tourner passe de « l'équipe unique qui l'a écrit » à « n'importe quel ado avec un VPS ». Mimikatz. EternalBlue. Le code source de Conti. À chaque fois, la capacité n'a pas changé — elle a simplement cessé d'être rare. Le gros titre de la semaine dernière, rapporté par BleepingComputer sur la foi des recherches d'OX Security, c'est que Shai-Hulud — le ver dont nous avions parlé quand il avait dévoré quarante-deux paquets @tanstack en six minutes — a rejoint cette liste.

Le fork n'est pas théorique. Dès dimanche, OX avait documenté quatre paquets malveillants publiés depuis un seul compte npm nommé deadcode09284814 :

  • chalk-tempalte — un typosquat de chalk-template, embarquant une copie quasi mot pour mot du code source divulgué de Shai-Hulud, y compris le comportement signature de Shai-Hulud consistant à téléverser les identifiants volés sous forme de dépôts GitHub publics auto-générés. La lecture d'OX : « le code du malware Shai-Hulud est une copie quasi exacte du code source divulgué, sans aucune technique d'obfuscation », ce qui suggère un nouvel acteur qui ne s'est même pas donné la peine de limer les numéros de série.
  • axois-utils — un typosquat de axios-utils, livrant une charge utile Golang qu'OX appelle Phantom Bot : floods HTTP, TCP, UDP et reset, persistance via le dossier de démarrage Windows et les tâches planifiées, et un C2 à b94b6bcfa27554.lhr.life. Votre machine de dev, enrôlée.
  • @deadcode09284814/axios-util — un typosquat différent, une charge utile différente : clés SSH, variables d'environnement, identifiants AWS/GCP/Azure, expédiés vers 80.200.28.28:2222. « Plutôt direct », pour reprendre les mots d'OX.
  • color-style-utils — un infostealer pur et simple qui récupère votre IP, votre géolocalisation et les données de portefeuille crypto, et envoie le tout en POST à edcf8b03c84634.lhr.life.

Téléchargements hebdomadaires cumulés au moment de la publication d'OX : 2 678.

Il y a deux histoires entremêlées ici qui méritent d'être démêlées. La barbante, c'est que npm a des typosquats. C'est le cas ; ça l'a toujours été ; ça le sera toujours, pour la même raison qu'il y a des chiens nommés Rex au parc — les noms sont gratuits et l'espace de noms est plat. L'intéressante, c'est que la barrière pour faire tourner un ver de la classe de Shai-Hulud vient de s'effondrer. Jusqu'à la semaine dernière, il fallait l'outillage de l'équipe d'origine, l'infrastructure de l'équipe d'origine, la discipline de l'équipe d'origine pour ne pas se faire prendre. Aujourd'hui, il faut un clone GitHub d'un dépôt public, un tunnel lhr.life et la patience de taper npm publish. Les quatre paquets trouvés par OX en sont, collectivement, la preuve.

Pourquoi le nombre d'acteurs compte plus que le nombre de paquets.

Un seul ver sophistiqué est, en un sens, un adversaire tractable. Il a des tics. Il a une infrastructure. Il a des habitudes. On peut écrire des signatures de détection contre lui. Aikido, Socket, Snyk, Wiz — les maisons de surveillance de la chaîne d'approvisionnement npm qui se sont jetées sur l'incident @tanstack du 11 mai — l'ont attrapé en quelques heures, précisément parce qu'elles surveillaient la même famille depuis huit mois.

Une famille de vers dérivés écrits par des gens qui ont téléchargé le code source depuis un site de partage de texte, c'est une autre forme. Chacun exfiltrera vers un C2 différent, embarquera une clé RSA différente, choisira une combinaison différente de fichiers à lire, et élira domicile dans un espace de typosquats différent. Certains seront prudents ; la plupart seront négligents d'une manière qui les fera prendre rapidement ; l'un d'entre eux, la prochaine fois qu'on écrira un billet comme celui-ci, sera assez sophistiqué pour qu'on ne l'attrape pas en une semaine. Le problème de détection des défenseurs s'élargit de « repérer Shai-Hulud » à « repérer tout ce qui veut lire ~/.ssh/id_ed25519 depuis l'intérieur d'un script prepare ». C'est une surface bien, bien plus large.

La forme du chemin d'installation a également changé, et c'est la partie qui devrait inquiéter quiconque fait tourner un agent de codage. Un développeur humain qui voulait chalk-template, en 2024, aurait lu le nom du paquet dans un tutoriel, l'aurait tapé, et aurait remarqué la coquille quand chalk-tempalte serait revenu avec deux cents téléchargements et un inconnu comme publicateur. Un agent de codage à qui on demande en 2026 « ajoute de la couleur à la sortie de mon CLI » installera ce que le gestionnaire de paquets lui renverra. L'agent ne voit pas le champ « publicateur ». L'agent ne remarque pas que le README fait trois lignes. L'agent fait trente npm install cette heure-ci parce que l'utilisateur fait le travail d'une petite équipe et que l'agent est payé à la tâche, pas à l'installation.

Ce que fait vraiment un clone de Shai-Hulud non obfusqué.

Le paquet chalk-tempalte est, écrit OX, « presque sans aucun changement » par rapport au code source divulgué. Cela signifie que les mêmes mécaniques que nous avons décortiquées dans Le ver qui s'écrit dans .claude s'appliquent, avec un nouveau pli significatif : le canal d'exfiltration est GitHub lui-même.

Le tour signature de Shai-Hulud — préservé par le copycat parce que copier est plus facile que réécrire — c'est que les identifiants volés ne partent pas vers un serveur de dépôt caché. Ils partent vers un dépôt GitHub public fraîchement créé, publié à l'aide d'un jeton GitHub que le malware vient juste de voler à la victime. Les secrets de la victime sont posés dans un dépôt public, possédé par le compte GitHub de la victime elle-même, à la portée de n'importe qui sur Terre, jusqu'à ce que quelqu'un remarque et que le dépôt soit détruit. Le playbook standard du défenseur — bloquer le domaine d'exfiltration, chercher du DNS sortant inhabituel — ne détecte pas cela, parce que le DNS sortant, c'est api.github.com, à qui votre machine de développeur parle deux cents fois par heure de toute façon. Le bundle d'identifiants quitte votre laptop déguisé en git push.

Une fois les secrets publics, n'importe qui qui surveille le flux GitHub — et plusieurs personnes surveillent le flux GitHub pour exactement cette raison — peut les ramasser. Le ver n'a pas besoin de maintenir son C2 en vie. GitHub est le C2.

UN COMPTE npm — deadcode09284814 — QUATRE CHARGES UTILESUTILISATEUR npmdeadcode09284814 inscrit : 2026-05 paquets : 4 DL hebdo : 2 678PUBLIÉSchalk-tempalte ↳ coquille : chalk-templateaxois-utils ↳ coquille : axios-utils@…/axios-util ↳ sosie scopécolor-style-utils ↳ nom génériqueles quatre embarquent un postinstallou un script preparechalk-tempalte → CLONE DE SHAI-HULUDlit : ~/.npmrc, ~/.config/gh, $GH_TOKEN, ~/.aws, ~/.sshcrée : un dépôt GitHub public sous le compte de la victimecommit : secrets, chiffrés avec la clé RSA publique embarquéeaxois-utils → PHANTOM BOT (DDoS Golang)persistance : dossier de démarrage Windows + tâche planifiéecapacité : flood HTTP/TCP/UDP, attaques par reset TCP@…/axios-util → INFOSTEALER DIRECTlit : clés SSH, variables d'env, identifiants AWS/GCP/AzurePOST : bundle brut, aucune couche de chiffrementcolor-style-utils → VOLEUR DE PORTEFEUILLESlit : IP, géolocalisation, portefeuilles d'extension navigateurcibles : coffres MetaMask, Phantom, Keplr sur disqueOÙ VA LE BUTINchalk-tempalte : api.github.com (jeton de la victime) nouveau dépôt PUBLIC sur le compte victime + 87e0bbc636999b.lhr.lifeaxois-utils : b94b6bcfa27554.lhr.life bot Golang, reçoit ses ordres@…/axios-util : 80.200.28.28:2222 TCP brut, sans TLScolor-style-utils : edcf8b03c84634.lhr.life POST HTTPStrois tunnels .lhr.life différents,une IP nue, un api.github.com :bloquer-le-domaine ne suffit pas.
Où les quatre paquets `deadcode09284814` emmènent les secrets qu'ils lisent. chalk-tempalte utilise le propre jeton GitHub volé à la victime pour publier un nouveau dépôt public contenant le butin ; le C2 à 87e0bbc636999b.lhr.life est secondaire. axois-utils enrôle la machine dans un botnet DDoS Golang (Phantom Bot) avec persistance via le dossier de démarrage Windows et les tâches planifiées, prenant ses ordres depuis b94b6bcfa27554.lhr.life. Les deux autres envoient en POST un bundle d'identifiants vers une IP louée et un hôte tunnelisé respectivement. Les quatre paquets exécutent leur charge utile depuis `prepare` ou post-install, ce qui signifie qu'ils s'exécutent au moment où l'agent tape `npm install`, pas au moment où l'utilisateur lit le diff.

Il y a un tout petit détail dans cette image qui mérite qu'on s'y attarde. Le clone de Shai-Hulud, chalk-tempalte, ne s'appuie même pas sur sa propre infrastructure pour recevoir le butin. Il utilise l'identité GitHub de la victime elle-même pour publier les secrets volés à la victime elle-même dans un dépôt public sur GitHub. Le C2 sur lhr.life n'est qu'une sauvegarde. Le canal principal, c'est git push. Pour un défenseur qui surveille l'egress, c'est indiscernable de la CI normale du développeur. Pour GitHub, c'est — jusqu'à ce que quelqu'un signale le dépôt — un projet public légitime appartenant à un vrai utilisateur. L'exfiltration est blanchie à travers l'identité de la victime.

Ce que fait l'agent pendant que vous faites défiler.

Si vous avez un agent de codage dans votre terminal — Claude Code, Cursor's CLI, Codex CLI, Aider, au choix — il y a une probabilité non nulle que, pendant le temps qu'il vous a fallu pour lire le dernier paragraphe, l'agent ait lancé un npm install en votre nom. Peut-être deux. Les agents de codage ne s'arrêtent pas pour admirer les arbres de dépendances. La raison même pour laquelle vous en avez acheté un, c'est qu'il ne s'arrête pas.

Les paquets qu'OX a attrapés sont des typosquats, une catégorie d'erreur très spécifiquement adaptée à la vitesse des machines. Un humain qui doit taper chalk-template mettra les lettres dans le bon ordre parce qu'il l'a fait cent fois. Un modèle qui a ingéré tous les posts de Stack Overflow sur Terre a vu chalk-template et chalk-tempalte dans le même corpus d'entraînement — ce dernier typiquement à l'intérieur d'une capture d'écran de l'erreur de quelqu'un d'autre — et, sur un prompt du genre « ajoute une sortie colorée à mon CLI », émettra parfois la coquille mot pour mot. L'agent ne sourcille pas. Le gestionnaire de paquets ne sourcille pas. Le script prepare s'exécute.

Ce n'est pas un mode de défaillance hypothétique. C'est le mode de défaillance pour lequel la famille Shai-Hulud a été conçue. Le ver original se propageait en volant des jetons de mainteneur et en s'en servant pour republier des versions compromises de paquets légitimement populaires. Les copycats n'ont pas encore les jetons de mainteneur ; ce qu'ils ont, c'est l'espace de noms des typosquats, dans lequel les agents sont particulièrement doués pour tomber.

Où Bromure s'inscrit dans cette histoire.

Bromure Agentic Coding est la configuration dans laquelle l'agent de codage tourne à l'intérieur d'une VM Linux jetable par tâche, avec le dossier projet monté, l'egress arbitré et les identifiants conservés côté hôte macOS de l'hyperviseur. Nous avons détaillé l'architecture dans le billet sur Bitwarden CLI et le billet @tanstack. Ce qui suit, c'est ce qui arrive précisément à chacun de ces quatre paquets à l'intérieur de cette frontière.

VM BROMURE PAR TÂCHE — ce que voient les quatre charges utilesAGENT DE CODAGE$ claude> ajoute couleur à mon clitool: bashnpm i chalk-tempalte↳ postinstall s'exécute↳ dans le guest↳ la trace l'enregistreSYSTÈME DE FICHIERS GUEST — stubs et absences~/.aws/credentialsaws_secret = stub-aws-…~/.npmrc$GH_TOKENghp_stub_…~/.ssh/id_ed25519Aucun fichier ou dossier de ce typecoffres MetaMask / Phantom / Keplrnon installésprofil navigateurpas de navigateur hôte dans le guest/etc/cron.d, dossier Startupsur disque jetableRÉSULTATS DES CHARGES UTILESchalk-tempalte : lit les stubs, tente api.github.com → 401axois-utils : le bot écrit dans le Startup guest → effacé au resetcolor-style-utils : aucun portefeuille présentHYPERVISEUR — courtier d'identifiants + proxy d'egressHÔTE macOS — vrais secrets et vrai navigateur, n'ont jamais traversé la frontièreVRAI COFFRE D'IDENTIFIANTSTrousseau macOSid_ed25519 (privée)~/.aws/credentialsAKIA… réel~/.config/gh/hosts.ymlghp_real (scopé à cette tâche)~/.npmrcnpm_… vrai jeton de publication~/Library/.../MetaMask/vault.json (vrai portefeuille)profil Chromium hôtevotre vrai navigateurrien de tout cela n'est accessible depuis le guestPROXY D'EGRESS — liste blanche uniquementgit push → api.github.com/votre/repo stub ghp_… ⇒ vrai ghp_… (en liste blanche)POST api.github.com/user/repos (CREATE) pas en liste blanche ⇒ 401 vers le guestPOST 87e0bbc636999b.lhr.life pas en liste blanche ⇒ bloqué, la trace l'enregistrePOST 80.200.28.28:2222 pas en liste blanche ⇒ bloqué, la trace l'enregistreRayon d'impact = une VM éphémère + ce qui se trouvait déjà dans le dossier projet monté. Reset, et la prochaine installation repart de zéro.
Le même npm install d'un des typosquats deadcode09284814, mais l'agent tourne à l'intérieur d'une VM Bromure par tâche. chalk-tempalte lit ~/.aws, ~/.npmrc, ~/.ssh à l'intérieur du guest et y trouve des stubs et des fichiers manquants. Il tente ensuite d'utiliser $GH_TOKEN pour créer un dépôt public — le jeton stub renvoie 401 au niveau du proxy d'egress, parce que le proxy ne substitue le vrai jeton que pour les endpoints en liste blanche que la tâche a demandés, et « créer un nouveau dépôt public » ne fait pas partie de cette liste. axois-utils s'enrôle dans le C2 de Phantom Bot ; le binaire de persistance Golang s'écrit dans le répertoire Startup du guest, qui réside sur le disque jetable. color-style-utils cherche un coffre MetaMask qui n'est pas installé. Le rayon d'impact, c'est une VM éphémère, plus ce qui se trouvait déjà dans le dossier projet, point.

Passons en revue les quatre paquets, un par un, parce que c'est dans les détails que l'architecture gagne sa place.

chalk-tempalte cherche $GH_TOKEN.

Le coup signature du clone de Shai-Hulud, c'est de prendre le jeton GitHub du développeur et de s'en servir pour créer un dépôt public sur le propre compte du développeur. À l'intérieur d'une VM Bromure, le $GH_TOKEN qu'il lit est un stub — une chaîne syntaxiquement valide qui commence par ghp_ et qui existe pour exactement cette raison. La première action du runner est POST /user/repos contre api.github.com. Le proxy d'egress côté hôte reconnaît api.github.com comme un endpoint en liste blanche, mais uniquement pour les opérations que la tâche courante a effectivement demandées — git push vers le dépôt sur lequel la tâche travaille, gh pr create contre ce même dépôt, gh api repos/that/repo/issues. « Créer un nouveau dépôt public sur le compte de l'utilisateur » ne fait pas partie de cette liste, parce que l'utilisateur ne l'a pas demandé. Le proxy refuse de substituer le vrai jeton, et le stub part en tant que stub. GitHub renvoie 401. Le canal d'exfiltration du ver — le canal malin, celui conçu pour contourner le filtrage DNS d'egress — ne s'ouvre jamais.

Le canal de secours, le tunnel lhr.life à 87e0bbc636999b.lhr.life, n'est pas non plus en liste blanche. La trace enregistre la tentative. Les octets ne quittent pas la machine.

axois-utils installe Phantom Bot pour la persistance.

Le bot Golang essaie de s'écrire dans le dossier de démarrage Windows et de créer une tâche planifiée. La VM Bromure est un guest Linux, donc la persistance spécifique à Windows est, pour ainsi dire gratuitement, un no-op. Sur une variante Linux de la même charge utile — que quelqu'un livrera, sous peu — le bot s'écrirait dans /etc/cron.d/ ou ~/.config/systemd/user/. Ces deux chemins se trouvent à l'intérieur du disque copy-on-write jetable du guest. Le prochain bromure reset, ou la fin naturelle de la tâche courante, fait tomber le disque. La persistance disparaît sans aucune chasse.

Pendant ce temps, la connexion sortante du bot vers b94b6bcfa27554.lhr.life n'est pas sur la liste blanche d'egress de la tâche, parce qu'aucune tâche de codage légitime ne parle à un tunnel lhr.life fraîchement enregistré. Le bot téléphone à la maison dans un socket fermé. La trace de la session enregistre la tentative — utile demain matin quand une liste d'IOC sera publiée.

@deadcode09284814/axios-util envoie des identifiants bruts en POST.

La plus simple des quatre charges utiles est aussi celle qui a le moins à attraper. Le runner lit le ~/.ssh, le ~/.aws du guest, les variables d'environnement, et les envoie en POST à 80.200.28.28:2222. Le dossier SSH est vide. Le fichier AWS est un stub. Les variables d'environnement sont soit des stubs, soit absentes. L'IP de destination n'est pas en liste blanche. Soit la connexion est bloquée au niveau du proxy, soit elle quitte l'hôte en transportant une charge utile de placeholders. Les deux résultats conviennent.

color-style-utils cherche des portefeuilles qui n'existent pas.

Le voleur de crypto est le paquet dont le modèle de menace suppose le plus clairement que le navigateur du développeur se trouve sur la même machine. Il lit des chemins comme ~/Library/Application Support/Google/Chrome/Default/Local Extension Settings/<MetaMask-id>/ et les équivalents pour Phantom et Keplr. Aucun de ces chemins n'existe sur la VM Bromure. La VM n'a pas votre profil Chrome. La VM n'a pas d'extension de portefeuille installée. La VM n'est, par conception, le navigateur principal de personne. Le runner trouve un dossier vide et passe à autre chose.

C'est la partie qui n'est pas une histoire d'identifiants qui vivent sur l'hôte. Les portefeuilles vivent sur l'hôte parce que, sur un laptop normal, le navigateur du développeur et l'agent de codage du développeur partagent un système de fichiers. Bromure ne rend pas le portefeuille plus solide ; il le rend inaccessible depuis l'endroit où le ver tourne. Le ver ne peut pas lire ce qui n'est pas sur son disque.

Ce qui fait encore mal.

Il y a des recoins de cette histoire où la VM par tâche de Bromure n'est pas une solution, et ils méritent d'être nommés à voix haute.

Le dossier projet est monté.

Les fichiers que le ver écrit dans le dossier projet — y compris la persistance de type .claude/router_runtime.js que nous avons couverte dans le billet @tanstack — sont durables au-delà des resets de tâche, parce que c'est tout l'intérêt de monter le dossier projet. La défense, ici, ce n'est pas la VM. C'est git status et un coup d'œil de cinq secondes au diff avant de pousser. La trace facilite le repérage des sessions qui ont ajouté des fichiers inattendus.

La liste blanche d'egress est étroite à dessein.

Le courtier d'identifiants de Bromure fonctionne parce que la liste blanche est étroite. Si vous mettez npm publish sur votre propre scope en liste blanche parce que vous publiez une release aujourd'hui, et qu'il se trouve que vous installez l'un de ces quatre paquets aujourd'hui, le ver publiera sous votre scope. Mettez en liste blanche ce dont la tâche a besoin. Pas un octet de plus.

Un jeton valable pour cette tâche reste un vrai jeton.

Le jeton GitHub stub est échangé contre un vrai au moment du fil pour les opérations que la tâche a mises en liste blanche. Si chalk-tempalte parvenait à convaincre l'agent de faire un git push vers le propre dépôt du projet, ce push passerait avec un vrai jeton. La frontière protège les identifiants. Elle ne relit pas le diff. Relisez le diff.

La détection est en aval de la trace.

La trace de session enregistre chaque commande shell, chaque écriture de fichier et chaque requête sortante. À elle seule, elle ne classe pas 87e0bbc636999b.lhr.life comme mauvaise. Elle enregistre que la requête a été faite. Quand OX publiera une nouvelle liste d'IOC demain matin, votre recherche prendra deux secondes. C'est la valeur que la trace ajoute — pas de la magie, juste des reçus.

Une dernière chose.

La fuite n'est pas la nouvelle. La nouvelle, c'est ce que la fuite rend probable au cours de la prochaine année, à savoir beaucoup de petits forks à moitié compétents d'un ver que, même sous sa forme compétente, l'écosystème npm a attrapé de justesse. Certains forks seront assez bruyants pour avoir droit à un billet. La plupart resteront dans le registre pendant une semaine, collecteront quelques milliers de npm install, et disparaîtront quand quelqu'un finira par déposer un signalement d'abus. Les deux mille six cent soixante-dix-huit installations qu'OX a comptées sur les paquets deadcode09284814 ne sont pas une valeur aberrante. C'est la moyenne.

La question honnête n'est pas « est-ce que mon équipe évitera chaque typosquat empoisonné dans npm ». Les agents tapent vite. Les noms sont gratuits. La question honnête, c'est : quand l'agent en installera un — et au cours de la prochaine année, dans une équipe qui utilise des agents, il le fera — est-ce que le script post-install trouvera les mains du développeur, ou trouvera-t-il une boîte Linux jetable avec des stubs dans les fichiers d'identifiants et un proxy qui ne sait pas qui il est.

Bromure Agentic Coding est la deuxième option. C'est gratuit, open-source, et livré aujourd'hui. L'arbre des forks va empirer avant de s'améliorer.