Shai-Hulud est un ver auto-propagatif qui infecte les paquets npm, une attaque de la chaîne d'approvisionnement logicielle qui recrute chaque victime pour le diffuser plus loin. Il s'exécute à l'installation d'un paquet, vole sur la machine les tokens npm, les tokens GitHub et les clés cloud, et utilise le token npm pour publier des versions infectées des autres paquets de la victime. Depuis la première vague de septembre 2025, il est revenu à plusieurs reprises et, en mai 2026, ses auteurs ont publié le code source : chaque vague depuis peut donc provenir d'un groupe différent.
L'essentiel des dégâts se produit en CI. Lors de la vague de novembre 2025, Wiz a constaté que seules 23 % des infections touchaient des machines de développeurs : les 77 % restantes se trouvaient dans des pipelines CI/CD, GitHub Actions en tête. Un build installe des centaines de paquets, détient les secrets nécessaires pour publier et déployer, et exécute chaque script d'installation sans que personne ne regarde.
Quand une nouvelle vague est annoncée, chaque équipe pose la même question : l'un de nos builds a-t-il installé une version infectée ? Cet article explique ce que fait le ver, pourquoi la réponse habituelle (« vérifier le lockfile ») est incomplète et comment y répondre correctement.
Les vagues jusqu'ici
| Quand | Nom | Ce qui a changé |
|---|---|---|
| Septembre 2025 | Shai-Hulud | Un script postinstall exécutait un bundle.js de 3,6 Mo, utilisait TruffleHog pour trouver des secrets et republiait jusqu'à 20 paquets du mainteneur. Plus de 500 paquets ont été touchés, dont @ctrl/tinycolor et 18 paquets @crowdstrike. |
| Novembre 2025 | Sha1-Hulud: The Second Coming | Passage à preinstall, installation du runtime Bun pour exécuter une charge utile de 10 Mo, porte dérobée dans jusqu'à 100 paquets par mainteneur et enregistrement des machines infectées comme runners GitHub auto-hébergés. Des paquets de Zapier, ENS, PostHog et Postman ont été touchés ; Wiz a recensé plus de 25 000 dépôts avec des secrets divulgués. |
| Décembre 2025 | Test « 3.0 » | Une version modifiée dans un seul paquet, sans propagation notable. |
| Mai 2026 | TanStack | 84 versions malveillantes de 42 paquets @tanstack. L'attaquant n'a jamais volé de token npm : un workflow pull_request_target, un cache Actions empoisonné et un token OIDC lu dans la mémoire du runner lui ont permis de publier via le trusted publishing de TanStack. |
| Mai 2026 | AntV | 639 versions de 323 paquets. Sur les runners Linux GitHub Actions, la charge utile lisait les secrets dans la mémoire du processus du runner, contournant le masquage des logs. GitHub a invalidé 61 274 tokens d'écriture npm. |
| Juin 2026 | Miasma | 32 paquets @redhat-cloud-services, publiés via des workflows injectés avec une provenance valide. Chaque infection portait une charge utile chiffrée de façon unique, ce qui a mis en échec la détection par hachage. |
| Août 2026 | CHAINDROP | Des paquets de cache populaires comme keyv, cacheable et cache-manager, avec des centaines de paquets touchés au total. Il persistait via les fichiers de configuration d'éditeurs et d'assistants d'IA présents dans le dépôt. |
La charge utile est revenue en septembre 2026, dans quatre paquets qui ont échappé à la nouvelle analyse anti-malware de npm à la publication.
Ce que fait le ver dans un job CI
Les détails changent d'une vague à l'autre, mais le schéma reste le même :
- Il s'exécute à l'installation. Un script
preinstalloupostinstalls'exécute dès quenpm installounpm ciatteint la version infectée. Rien dans votre code n'a besoin de l'importer. - Il collecte des secrets. Les variables d'environnement, les tokens
.npmrc, les identifiants cloud issus du service de métadonnées et des gestionnaires de secrets, et tout ce que TruffleHog trouve sur le disque. Les vagues suivantes ont lu directement la mémoire du runner GitHub Actions, ce qui donne accès à des secrets jamais exposés à l'étape et que le masquage aurait cachés dans les logs. - Il les fait sortir. Les premières vagues créaient des dépôts GitHub publics portant le nom du ver et y poussaient un workflow qui envoyait chaque secret du dépôt vers
webhook.site. Les vagues suivantes ont utilisé des serveurs de l'attaquant et des réseaux de messagerie. - Il se propage et s'installe. Avec un token npm volé, il publie des versions infectées d'autres paquets. Avec un token GitHub, il ajoute des workflows, enregistre des runners auto-hébergés ou dépose des fichiers de configuration qui s'exécutent la prochaine fois que quelqu'un ouvre le dépôt.
- Il se comporte différemment en CI. La charge utile de novembre 2025 recherchait
GITHUB_ACTIONS,BUILDKITE,CIRCLE_SHA1et des variables similaires et, en CI, s'exécutait de façon synchrone, terminant son travail avant que le build ne continue.
Pourquoi le lockfile ne tranche pas
Le conseil habituel est de chercher les versions affectées dans package-lock.json, yarn.lock ou pnpm-lock.yaml. Faites-le d'abord, mais un lockfile sain ne prouve pas qu'un build est sain :
- Des paquets installés hors du lockfile.
npx some-toolinstalle dans le cache npm un paquet qui n'est pas une dépendance du projet.npm install -gdans une étape de configuration, un outil installé dans un script de build et unnpm installdans un Dockerfile contournent tous le lockfile du projet. - Le lockfile d'aujourd'hui sur la branche n'est pas celui qui a tourné. Un build d'il y a trois semaines a utilisé ce que le lockfile indiquait alors, sur la branche qui l'a déclenché. Les pull requests de mise à jour de dépendances exécutent la CI sur de nouvelles versions avant que quiconque ne les relise.
- Les caches restaurent ce qui a été installé auparavant. Un
node_modulesou un cache npm restauré peut contenir une version que le lockfile actuel ne mentionne plus. npm ciinstalle ce que dit le lockfile. Si la version infectée était déjà verrouillée, une installation stricte l'installe fidèlement.- Une version correctement publiée peut quand même être malveillante. TanStack et Miasma ont été compromis via leurs propres workflows de release, et les versions infectées de Miasma portaient une provenance valide. Vérifier qu'une version a été publiée correctement ne vous dit pas qu'elle est saine.
Ce dont vous avez réellement besoin, c'est d'une trace des versions de paquets que chaque build a récupérées, sur tous les dépôts et toutes les branches, pendant toute la période où une version infectée se trouvait sur le registre.
Comment vérifier si vous avez été touché
- Obtenez la liste des versions affectées pour la vague. Les éditeurs de sécurité les publient en quelques heures ; Datadog, par exemple, tient à jour un dépôt d'indicateurs de compromission avec des listes de paquets et de versions.
- Cherchez dans chaque lockfile, sur chaque branche où la CI a tourné pendant la période, pas seulement sur la branche par défaut.
- Cherchez dans les builds eux-mêmes. Regardez dans les logs de CI
setup_bun.js,bun_environment.jsou d'autres noms de fichiers cités dans l'avis, ainsi que les sorties d'installation qui mentionnent les versions affectées. Si vous conservez des SBOM par build, cherchez-y les versions. - Vérifiez votre organisation GitHub pour repérer de nouveaux dépôts portant le nom du ver, des fichiers de workflow que vous n'avez pas écrits (par exemple
shai-hulud-workflow.yml,discussion.yamlouformatter_*.yml) et des runners auto-hébergés que vous n'avez pas enregistrés. - Videz les caches. Supprimez les caches GitHub Actions et le cache npm sur les machines des développeurs et les runners auto-hébergés, pour que le prochain build installe des versions saines.
- Si un build a installé une version affectée, considérez le job comme compromis. Renouvelez chaque secret qu'il pouvait lire : tokens npm, tokens GitHub, identifiants cloud et clés de déploiement. Comme les vagues suivantes ont publié des secrets volés dans les comptes d'autres victimes, ne vous croyez pas à l'abri parce que rien n'est apparu dans le vôtre.
Comment réduire les dégâts la prochaine fois
- Désactivez les scripts d'installation quand c'est possible. npm v12, publié en juillet 2026, désactive les scripts d'installation par défaut et vous permet de les autoriser par paquet. pnpm et Yarn offrent des contrôles similaires.
- Attendez avant d'adopter de nouvelles versions. La plupart des versions infectées sont trouvées et retirées en quelques heures ou quelques jours. Un délai de carence sur les mises à jour de dépendances, que Dependabot et Renovate prennent tous deux en charge, les tient à l'écart de vos builds.
- Publiez avec le trusted publishing et des tokens à courte durée de vie. npm a révoqué tous les tokens classiques en décembre 2025. Utilisez le trusted publishing basé sur OIDC depuis la CI plutôt que des tokens à longue durée de vie, et séparez les workflows de publication des workflows de build et de test.
- Durcissez les workflows dont le ver abuse. Évitez
pull_request_target, épinglez les actions à des SHA de commit et ne donnez àGITHUB_TOKENque les permissions dont chaque job a besoin. Consultez Secrets GitHub Actions : comment ils fuient et comment les verrouiller. - Limitez les connexions des builds. Un build a besoin de ses registres de paquets et de quelques API. S'il ne peut atteindre que ceux-ci, les appels du ver vers
webhook.siteou ses propres serveurs échouent. Découvrez comment une politique de trafic sortant empêche l'exfiltration depuis GitHub Actions. Cela n'arrête pas l'exfiltration via un hôte que le build utilise légitimement, comme l'API de GitHub elle-même, sur laquelle certaines vagues se sont appuyées.
La place de CRACI
CRACI exécute vos jobs GitHub Actions sur ses propres runners (runs-on: craci), et un proxy qui comprend les paquets, sur le runner, enregistre chaque paquet que chaque job récupère réellement depuis npm et d'autres registres, y compris les paquets restaurés depuis des caches. Cet enregistrement couvre les angles morts ci-dessus : les outils npx, les installations globales et les installations dans des scripts de build apparaissent tous, car la trace vient de ce que le job a téléchargé, et non de ce qu'un lockfile a déclaré.
Quand une nouvelle vague est annoncée, vous recherchez les versions affectées dans les builds enregistrés par CRACI au lieu de les reconstituer à partir des lockfiles et des logs. Les résultats sont agrégés sur l'ensemble des builds et des dépôts, l'inventaire de CRACI montre quelles versions sont entrées dans quels produits, et la trace réseau de chaque job montre à quoi il s'est connecté : vous pouvez voir si un job qui a installé une version infectée a aussi atteint une destination qu'il n'aurait pas dû atteindre.
La politique de trafic sortant de CRACI limite les connexions d'un job, est validée avant le démarrage du job, bloque tout en cas d'échec et vous alerte quand un job tente d'atteindre une destination que la politique n'autorise pas. CRACI n'évalue pas les paquets selon leur comportement malveillant ; associez-le à un outil de réputation de paquets si vous voulez que les versions infectées soient bloquées avant l'installation.
Réservez une démo pour voir la trace des paquets de l'un de vos propres builds.
