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. Si l'un de vos builds a installé une version infectée, considérez ce job comme compromis et renouvelez chaque secret qu'il pouvait lire.
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, le reste se trouvant surtout 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. Aussi, 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 ?
Ce qui s'est passé
| Quand | Vague | 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. La CISA a publié une alerte le 23 septembre. |
| 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. Suivie sous CVE-2026-45321, ajoutée au catalogue KEV de la CISA le 27 mai. |
| 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. |
| Septembre 2026 | Résurgence | Le 7 septembre, la charge utile est revenue dans quatre paquets qui ont échappé à la nouvelle analyse anti-malware de npm à la publication. |
| Octobre 2026 | tensorlake | Le 8 octobre, [email protected] a été publié avec le ver. OX Security a observé de nouvelles clés d'attaquant et évoque « un acteur malveillant indépendant ou un nouveau groupe utilisant le nom Shai-Hulud ». |
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.
Qui est concerné
Toute personne dont les machines ou les builds ont installé une version infectée pendant sa présence sur le registre : ordinateurs portables de développeurs, jobs CI, builds Docker qui exécutent npm install et runners auto-hébergés. Nul besoin de dépendre directement du paquet : une dépendance transitive ou un outil lancé avec npx suffit. Chaque vague a sa propre liste de paquets et de versions, et les versions infectées sont généralement retirées en quelques heures ou quelques jours : ce qui compte, c'est de savoir si quelque chose les a installées pendant cette fenêtre.
Comment ça marche
Shai-Hulud n'est pas un bogue de npm. C'est du code malveillant publié à l'intérieur de paquets par ailleurs légitimes, que les avis de malware de GitHub pour les paquets infectés classent comme code malveillant intégré (CWE-506). Chaque vague combine quelques techniques :
- Exécution à l'installation. npm exécute les scripts de cycle de vie d'un paquet, comme
preinstall,postinstalletprepare, pendant l'installation, avec les permissions de l'utilisateur ou du job CI. Il suffit qu'une version infectée figure dans l'arbre des dépendances ; rien n'a besoin de l'importer. - Propagation grâce à des droits de publication volés. Avec un token npm ou GitHub dérobé à une victime, le ver publie de nouvelles versions des autres paquets de cette victime en y ajoutant la charge utile : chaque mainteneur infecté infecte ainsi d'autres paquets.
- Publication via le pipeline de release de la victime elle-même (TanStack, mai 2026). Selon l'avis de GitHub, l'attaquant a enchaîné un workflow
pull_request_target, un cache GitHub Actions empoisonné par-delà la frontière de confiance entre le fork et le dépôt de base, et un token OIDC lu dans la mémoire du runner, puis a publié via le trusted publishing de TanStack sans modifier le workflow de publication. Les versions infectées déclaraient une dépendance optionnelle vers un commit d'un fork, dont le scriptprepareexécutait la charge utile puis échouait volontairement : npm l'écartait alors et ne laissait aucune trace dansnode_modules. - Provenance valide (Miasma, juin 2026). Des workflows injectés publiaient depuis les propres dépôts des victimes, si bien que les versions infectées portaient une provenance valide, et une charge utile chiffrée de façon unique pour chaque infection mettait en échec la détection par hachage.
Comment vérifier si vous êtes concerné
- 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.
Des commandes à adapter. Pour chercher dans les lockfiles de chaque branche distante, placez les versions affectées par la vague dans affected.txt, un name@version par ligne :
git fetch --all --quiet
for ref in $(git for-each-ref --format='%(refname:short)' refs/remotes); do
while read -r pkg; do
name=${pkg%@*}; version=${pkg##*@}
git grep -n -A3 -F "$name" "$ref" -- '*package-lock.json' '*yarn.lock' '*pnpm-lock.yaml' \
| grep -F "$version" | sed "s|^|$pkg |"
done < affected.txt
done
Pour lister les runners auto-hébergés de votre organisation, les nouveaux dépôts qui mentionnent le ver et les fichiers de workflow que le ver est connu pour créer (remplacez ORG) :
gh api --paginate orgs/ORG/actions/runners --jq '.runners[] | [.name, .os, .status] | @tsv'
gh repo list ORG --limit 1000 --json name,description,createdAt \
--jq '.[] | select((.description // "") | test("hulud"; "i")) | [.name, .createdAt] | @tsv'
for repo in $(gh repo list ORG --limit 1000 --json name --jq '.[].name'); do
gh api "repos/ORG/$repo/contents/.github/workflows" --jq '.[].name' 2>/dev/null \
| grep -E '^(shai-hulud-workflow\.yml|discussion\.yaml|formatter_.*\.yml)$' \
| sed "s|^|$repo |"
done
Les dépôts peuvent aussi avoir leurs propres runners (gh api repos/ORG/REPO/actions/runners), et les premières vagues créaient des dépôts dans les comptes personnels des développeurs dont elles avaient volé les tokens : demandez à vos développeurs de vérifier les leurs.
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. Consultez sommes-nous concernés ? Comment évaluer une nouvelle CVE sur l'ensemble de vos builds.
Comment corriger, et que faire si vous ne pouvez pas encore appliquer le correctif
- 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.
- 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 :
gh cache delete --all --repo ORG/REPO # GitHub Actions caches, per repository
npm cache clean --force
rm -rf node_modules "$(pnpm store path)" # pnpm; for Yarn: yarn cache clean
- Épinglez une version saine et supprimez les workflows, dépôts, runners et fichiers de configuration ajoutés par le ver.
Vérifier le nettoyage
- Chaque lockfile se résout vers des versions saines. Sur chaque branche,
npm ls <package>(oupnpm whyouyarn why) montre la version installée ; aucune ne doit figurer sur la liste de la vague. - Une installation propre ne contient aucun fichier de charge utile. Après la purge des caches ci-dessus, réinstallez et cherchez les fichiers de charge utile cités dans les avis :
find node_modules -name bundle.js -size +3M \
-o -name setup_bun.js -o -name bun_environment.js -o -name router_init.js
- Les anciens identifiants ne fonctionnent plus. Les tokens npm révoqués ont disparu de
npm token list, les anciens tokens GitHub et clés de déploiement sont supprimés, et les clés cloud sont désactivées, pas seulement remplacées. - Les caches ont disparu.
gh cache listne montre, dans aucun dépôt affecté, de cache créé avant le nettoyage. - Il ne reste rien de ce que le ver a ajouté : aucun dépôt, fichier de workflow, runner auto-hébergé ni fichier de configuration d'éditeur ou d'assistant d'IA que vous n'avez pas créé.
- Le prochain build est sain. Vérifiez les versions qu'il a réellement installées, d'après son log ou sa SBOM par build, et pas seulement d'après le lockfile.
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. Le package firewall de GitHub, en préversion publique pour GitHub Enterprise Cloud depuis le 20 octobre 2026, vérifie les installations npm à la recherche de paquets malveillants et propose lui aussi des délais de carence.
- 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.
Rechercher une compromission
Shai-Hulud est malveillant par conception : toute machine ou tout job qui a installé une version infectée l'a exécutée. Les indicateurs varient selon la vague : utilisez l'avis de la vague que vous vérifiez. D'après les sources primaires :
| Vague | Indicateurs | Source |
|---|---|---|
| Septembre 2025 | Charge utile bundle.js, SHA-256 46faab8ab153fae6e80e7cca38eab363075bb524edd79e42269217a083628f09 ; workflow .github/workflows/shai-hulud-workflow.yml sur une branche shai-hulud ; un dépôt public nommé Shai-Hulud ; connexions vers webhook.site | StepSecurity, CISA |
| Novembre 2025 | setup_bun.js, SHA-1 d1829b4708126dcc7bea7437c04d1f10eacd4a16 ; bun_environment.js, SHA-1 d60ec97eea19fffb4809bc35b91033b52490ca11 ou 3d7570d14d34b0ba137d502f042b27b0f37a59fa ; dépôts contenant cloud.json, contents.json, environment.json, truffleSecrets.json et actionsSecrets.json ; workflows .github/workflows/discussion.yaml et formatter_*.yml ; un runner auto-hébergé nommé SHA1HULUD | Wiz |
| Mai 2026, TanStack | "@tanstack/setup": "github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c" dans optionalDependencies ; charge utile router_init.js (environ 2,3 Mo) à la racine du paquet ; script auxiliaire tanstack_runner.js ; connexions vers filev2.getsession.org et de seed1.getsession.org à seed3.getsession.org, et téléchargements de second niveau depuis litter.catbox.moe | GHSA-g7cv-rxg3-hmpx |
Les vagues suivantes ont modifié la charge utile à chaque infection : un hachage de fichier qui ne correspond pas ne prouve donc rien. Comparez d'abord les versions de paquets.
Quand supposer une compromission. Considérez comme compromis chaque hôte et chaque job CI qui a installé une version affectée, même si l'installation a échoué ou n'a rien laissé dans node_modules. L'avis de GitHub sur TanStack indique que tout environnement de développement ou de CI qui a installé une version affectée « doit être considéré comme compromis ». Vérifiez à partir de la première publication de la version infectée, et non à partir du jour où la vague a été annoncée.
Renouvelez les identifiants. Renouvelez tout ce que l'installation pouvait atteindre. Dans la vague TanStack, cela concernait les identifiants AWS, GCP, Kubernetes, Vault, GitHub, npm et SSH, selon le postmortem. Examinez les journaux d'audit cloud pour repérer l'activité des hôtes affectés pendant et après la fenêtre d'installation.
Purgez les caches et les artefacts. Cherchez dans les dépôts d'artefacts et les proxys de dépendances les copies en cache des versions affectées et supprimez-les, comme le conseille la CISA. Supprimez node_modules, le cache npm et les caches de CI ; dans la vague TanStack, la porte d'entrée de l'attaquant était un cache de store pnpm empoisonné : vérifiez donc les caches Actions autant que les caches de paquets. Reconstruisez et republiez tout ce qu'un job compromis a produit.
Que dire à vos clients
Si vous livrez un logiciel construit avec npm, vos clients demanderont si votre produit contient le ver ou si votre pipeline de release a été compromis. Répondez avec ce que vous avez vérifié et ce que vous avez fait :
Nous avons comparé chaque build de [produit] entre le [date] et le [date] aux listes publiées des versions de paquets npm infectées par Shai-Hulud. [Aucun build n'a installé de version affectée.] / [Des builds de [composant] du [date] ont installé [paquet@version] ; nous avons renouvelé les identifiants auxquels ces builds avaient accès, reconstruit [produit] à partir de dépendances saines et publié la version [version]. Les versions [x à y] sont affectées ; veuillez passer à la version [version].] Nous mettrons à jour cet avis si de nouvelles vagues changent la situation.
Si vous êtes encore en train de vérifier, dites-le, et dites quand vous répondrez. Pour une première déclaration et des modèles pour chaque cas de figure, consultez comment répondre aux clients après une attaque de la chaîne d'approvisionnement.
Comment CRACI vous aide
CRACI exécute vos jobs GitHub Actions sur ses propres runners (runs-on: craci) et 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, et la trace réseau de chaque job montre à quoi il s'est connecté. L'historique des builds conserve chaque build passé avec sa SBOM : vous voyez ainsi quels builds contenaient une version infectée et la période pendant laquelle vous étiez exposé, dans la limite de votre durée de conservation.
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. Les versions infectées dotées d'une CVE publiée, comme celles de la vague TanStack, sont signalées comme toute autre vulnérabilité, et les policy gates peuvent bloquer les builds qui les contiennent. CRACI n'évalue pas les paquets selon leur comportement malveillant et ne signale pas une nouvelle version infectée avant qu'un avis n'existe ; associez-le à un outil de réputation de paquets si vous voulez que les versions infectées soient bloquées avant l'installation.
Voyez la trace des paquets de l'un de vos propres builds
Autres incidents : toutes les vulnérabilités.
Sources et mises à jour
Sources primaires et recherche originale :
- CISA : alerte du 23 septembre 2025, catalogue KEV
- GitHub : GHSA-g7cv-rxg3-hmpx (CVE-2026-45321), notre plan pour une chaîne d'approvisionnement npm plus sûre, révocation des tokens classiques npm
- Postmortem de TanStack
- Wiz sur la vague de novembre 2025 et ses suites
- Microsoft sur AntV et les recommandations de détection de Microsoft
- Datadog et Elastic sur CHAINDROP
- StepSecurity sur la vague de septembre 2025
- FIRST : score EPSS de CVE-2026-45321
Mises à jour :
-
2026-11-11: Ajout dans l'encadré de l'EPSS, de la classe CWE et du crédit, de vérifications prêtes à copier, et de sections sur le fonctionnement du ver, la vérification du nettoyage et la recherche d'une compromission, avec des indicateurs par vague.
-
2026-11-11: Déplacé du blog vers la section des vulnérabilités. Ajout de la vague tensorlake du 8 octobre 2026, de la date de la résurgence de septembre 2026, de la CVE-2026-45321 et de son inscription au catalogue KEV pour la vague TanStack, ainsi que du package firewall de GitHub.
-
2026-09-25: Première publication sur le blog de CRACI.