131% de CVE signalées en plus par jour en 2026 par rapport à l'an dernier.
← Toutes les vulnérabilités

Shai-Hulud : le ver npm et comment vérifier si vos builds sont concernés

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é

QuandVagueCe qui a changé
Septembre 2025Shai-HuludUn 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 2025Sha1-Hulud: The Second ComingPassage à 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 2025Test « 3.0 »Une version modifiée dans un seul paquet, sans propagation notable.
Mai 2026TanStack84 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 2026AntV639 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 2026Miasma32 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 2026CHAINDROPDes 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 2026RésurgenceLe 7 septembre, la charge utile est revenue dans quatre paquets qui ont échappé à la nouvelle analyse anti-malware de npm à la publication.
Octobre 2026tensorlakeLe 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 :

  1. Il s'exécute à l'installation. Un script preinstall ou postinstall s'exécute dès que npm install ou npm ci atteint la version infectée. Rien dans votre code n'a besoin de l'importer.
  2. 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.
  3. 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.
  4. 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.
  5. Il se comporte différemment en CI. La charge utile de novembre 2025 recherchait GITHUB_ACTIONS, BUILDKITE, CIRCLE_SHA1 et 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, postinstall et prepare, 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 script prepare exécutait la charge utile puis échouait volontairement : npm l'écartait alors et ne laissait aucune trace dans node_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é

  1. 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.
  2. Cherchez dans chaque lockfile, sur chaque branche où la CI a tourné pendant la période, pas seulement sur la branche par défaut.
  3. Cherchez dans les builds eux-mêmes. Regardez dans les logs de CI setup_bun.js, bun_environment.js ou 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.
  4. 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.yaml ou formatter_*.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-tool installe dans le cache npm un paquet qui n'est pas une dépendance du projet. npm install -g dans une étape de configuration, un outil installé dans un script de build et un npm install dans 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_modules ou un cache npm restauré peut contenir une version que le lockfile actuel ne mentionne plus.
  • npm ci installe 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

  1. 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.
  2. 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
  1. É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

  1. Chaque lockfile se résout vers des versions saines. Sur chaque branche, npm ls <package> (ou pnpm why ou yarn why) montre la version installée ; aucune ne doit figurer sur la liste de la vague.
  2. 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
  1. 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.
  2. Les caches ont disparu. gh cache list ne montre, dans aucun dépôt affecté, de cache créé avant le nettoyage.
  3. 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éé.
  4. 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_TOKEN que 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.site ou 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 :

VagueIndicateursSource
Septembre 2025Charge 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.siteStepSecurity, CISA
Novembre 2025setup_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é SHA1HULUDWiz
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.moeGHSA-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 :

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.

Questions fréquentes

Shai-Hulud est-il toujours actif ?

Oui. Il revient par vagues depuis septembre 2025, et ses auteurs ont publié le code source en mai 2026 : les vagues suivantes peuvent donc venir de groupes différents. La vague la plus récente couverte ici a touché le paquet tensorlake le 8 octobre 2026.

Shai-Hulud a-t-il une CVE ?

Le ver dans son ensemble n'en a pas. GitHub publie des avis de malware pour chaque paquet infecté, et la vague TanStack de mai 2026 porte la CVE-2026-45321, que la CISA a ajoutée à son catalogue Known Exploited Vulnerabilities le 27 mai 2026.

Un lockfile sain signifie-t-il que nous n'avons pas été touchés ?

Non. Les paquets installés avec npx, les installations globales, les installations dans des scripts de build et des Dockerfiles, d'anciens lockfiles sur d'autres branches et les caches restaurés peuvent tous introduire une version infectée que le lockfile actuel ne montre pas. Il vous faut une trace de ce que chaque build a réellement récupéré.

Comment vérifier si je suis concerné ?

Obtenez les versions de paquets affectées par la vague, cherchez dans chaque lockfile de chaque branche où la CI a tourné pendant la période, cherchez les versions et les noms de fichiers de la charge utile dans les logs de CI et les SBOM par build, et vérifiez votre organisation GitHub pour repérer des dépôts, des workflows et des runners auto-hébergés que vous n'avez pas créés.

Que faire si un build a installé une version infectée ?

Considérez le job comme compromis. Renouvelez chaque secret qu'il pouvait lire, y compris les tokens npm, les tokens GitHub, les identifiants cloud et les clés de déploiement, supprimez les caches GitHub Actions et les caches npm, et cherchez les workflows, dépôts et runners ajoutés par le ver.

Comment savoir si le ver s'est exécuté dans nos builds ?

Cherchez dans les logs de build, les lockfiles et les paquets installés les indicateurs de la vague : des fichiers de charge utile comme bundle.js, setup_bun.js, bun_environment.js ou router_init.js, des dépôts et des workflows que vous n'avez pas créés, un runner auto-hébergé nommé SHA1HULUD et des connexions vers webhook.site ou getsession.org. Tout job qui a installé une version affectée a exécuté la charge utile : considérez-le comme compromis même si vous ne trouvez rien d'autre.