Shai-Hulud is een zichzelf verspreidende worm die npm-pakketten infecteert, een aanval op de softwareketen die elk slachtoffer inzet om hem verder te verspreiden. Hij draait wanneer een pakket wordt geïnstalleerd, steelt npm-tokens, GitHub-tokens en cloudsleutels van de machine, en gebruikt het npm-token om geïnfecteerde versies van de andere pakketten van het slachtoffer te publiceren. Sinds de eerste golf in september 2025 is hij herhaaldelijk teruggekomen, en in mei 2026 publiceerden de makers de broncode, dus elke golf sindsdien kan van een andere groep zijn. Heeft een van uw builds een geïnfecteerde versie geïnstalleerd, behandel die job dan als gecompromitteerd en roteer elk secret dat hij kon lezen.
De meeste schade ontstaat in CI. Bij de golf van november 2025 ontdekte Wiz dat slechts 23% van de infecties op machines van ontwikkelaars zat, de rest vooral in CI/CD-pipelines, met GitHub Actions als meest voorkomende. Een build installeert honderden pakketten, bevat de secrets die nodig zijn om te publiceren en te deployen, en voert elk installatiescript uit zonder dat iemand meekijkt. Wanneer een nieuwe golf wordt aangekondigd, stelt elk team dus dezelfde vraag: heeft een van onze builds een geïnfecteerde versie geïnstalleerd?
Wat er gebeurde
| Wanneer | Golf | Wat er anders was |
|---|---|---|
| September 2025 | Shai-Hulud | Een postinstall-script voerde een bundle.js van 3,6 MB uit, gebruikte TruffleHog om secrets te vinden en publiceerde tot 20 pakketten van de maintainer opnieuw. Meer dan 500 pakketten werden getroffen, waaronder @ctrl/tinycolor en 18 @crowdstrike-pakketten. CISA publiceerde op 23 september een waarschuwing. |
| November 2025 | Sha1-Hulud: The Second Coming | Verhuisde naar preinstall, installeerde de Bun-runtime om een payload van 10 MB uit te voeren, voorzag tot 100 pakketten per maintainer van een backdoor en registreerde geïnfecteerde machines als self-hosted GitHub-runners. Pakketten van Zapier, ENS, PostHog en Postman werden getroffen; Wiz telde meer dan 25.000 repositories met gelekte secrets. |
| December 2025 | "3.0"-test | Een aangepaste versie in één pakket, zonder noemenswaardige verspreiding. |
| Mei 2026 | TanStack | 84 kwaadaardige versies van 42 @tanstack-pakketten. De aanvaller stal nooit een npm-token: een pull_request_target-workflow, een vergiftigde Actions-cache en een OIDC-token dat uit het runnergeheugen werd gelezen, stelden hem in staat te publiceren via TanStacks eigen trusted publishing. Geregistreerd als CVE-2026-45321, op 27 mei toegevoegd aan de KEV-catalogus van CISA. |
| Mei 2026 | AntV | 639 versies van 323 pakketten. Op Linux-runners van GitHub Actions las de payload secrets uit het geheugen van het runnerproces, waardoor log-masking werd omzeild. GitHub maakte 61.274 npm-schrijftokens ongeldig. |
| Juni 2026 | Miasma | 32 @redhat-cloud-services-pakketten, gepubliceerd via geïnjecteerde workflows met geldige herkomstgegevens. Elke infectie bevatte een uniek versleutelde payload, waardoor detectie op basis van hashes faalde. |
| Augustus 2026 | CHAINDROP | Populaire cachingpakketten zoals keyv, cacheable en cache-manager, met in totaal honderden getroffen pakketten. Hij bleef aanwezig via configuratiebestanden van editors en AI-assistenten in de repository. |
| September 2026 | Terugkeer | Op 7 september kwam de payload terug in vier pakketten die langs de nieuwe malwarescan van npm bij publicatie kwamen. |
| Oktober 2026 | tensorlake | Op 8 oktober werd [email protected] met de worm gepubliceerd. OX Security zag nieuwe sleutels van de aanvaller en vermoedt "een onafhankelijke dreigingsactor of een nieuwe groep die de naam Shai-Hulud gebruikt". |
Wat de worm in een CI-job doet
De details veranderen per golf, maar het patroon blijft:
- Hij draait bij de installatie. Een
preinstall- ofpostinstall-script wordt uitgevoerd zodranpm installofnpm cide geïnfecteerde versie bereikt. Niets in uw code hoeft hem te importeren. - Hij verzamelt secrets. Omgevingsvariabelen,
.npmrc-tokens, cloudinloggegevens uit de metadataservice en secretmanagers, en alles wat TruffleHog op schijf vindt. Latere golven lazen rechtstreeks het geheugen van de GitHub Actions-runner, wat secrets oplevert die nooit aan de stap zijn blootgesteld en die masking in logs had verborgen. - Hij voert ze af. Vroege golven maakten openbare GitHub-repositories aan die naar de worm waren genoemd en pushten een workflow die elk repositorysecret naar
webhook.sitestuurde. Latere golven gebruikten servers van aanvallers en berichtennetwerken. - Hij verspreidt zich en blijft. Met een gestolen npm-token publiceert hij geïnfecteerde versies van andere pakketten. Met een GitHub-token voegt hij workflows toe, registreert hij self-hosted runners of plaatst hij configuratiebestanden die draaien zodra iemand de repository opent.
- Hij gedraagt zich anders in CI. De payload van november 2025 controleerde op
GITHUB_ACTIONS,BUILDKITE,CIRCLE_SHA1en vergelijkbare variabelen, en draaide in CI synchroon, zodat hij zijn werk afrondde voordat de build verderging.
Wie getroffen is
Iedereen van wie de machines of builds een geïnfecteerde versie installeerden terwijl die in het register stond: laptops van ontwikkelaars, CI-jobs, Docker-builds die npm install uitvoeren en self-hosted runners. U hoeft niet direct van het pakket af te hangen; een transitieve afhankelijkheid of een tool die met npx draait, is genoeg. Elke golf heeft een eigen lijst van pakketten en versies, en geïnfecteerde versies worden meestal binnen uren of dagen verwijderd. Waar het om gaat, is dus of iets ze in dat tijdvenster heeft geïnstalleerd.
Hoe het werkt
Shai-Hulud is geen fout in npm. Het is kwaadaardige code die wordt gepubliceerd in verder legitieme pakketten, en die de malware-adviezen van GitHub voor de geïnfecteerde pakketten classificeren als ingebedde kwaadaardige code (CWE-506). Elke golf combineert een paar technieken:
- Uitvoering bij de installatie. npm voert de lifecyclescripts van een pakket, zoals
preinstall,postinstallenprepare, tijdens de installatie uit, met de rechten van de gebruiker of de CI-job. Een geïnfecteerde versie hoeft alleen in de afhankelijkheidsboom te staan; niets hoeft hem te importeren. - Verspreiding met gestolen publicatierechten. Met een npm- of GitHub-token dat van een slachtoffer is gestolen, publiceert de worm nieuwe versies van de andere pakketten van dat slachtoffer met de payload erin, zodat elke geïnfecteerde maintainer meer pakketten infecteert.
- Publicatie via de eigen releasepipeline van het slachtoffer (TanStack, mei 2026). Volgens het advies van GitHub koppelde de aanvaller een
pull_request_target-workflow, een GitHub Actions-cache die over de vertrouwensgrens tussen fork en basisrepository heen was vergiftigd, en een OIDC-token dat uit het geheugen van de runner werd gelezen, en publiceerde hij via het trusted publishing van TanStack zonder de publicatieworkflow te wijzigen. De geïnfecteerde versies declareerden een optionele afhankelijkheid van een commit in een fork, waarvan hetprepare-script de payload uitvoerde en daarna opzettelijk faalde, zodat npm hem verwierp en er geen spoor innode_modulesachterbleef. - Geldige herkomstgegevens (Miasma, juni 2026). Geïnjecteerde workflows publiceerden vanuit de eigen repositories van de slachtoffers, zodat de geïnfecteerde versies geldige herkomstgegevens hadden, en een per infectie uniek versleutelde payload maakte detectie op basis van hashes onmogelijk.
Hoe u controleert of u getroffen bent
- Haal de lijst met getroffen versies op voor de golf. Beveiligingsleveranciers publiceren die binnen enkele uren; Datadog onderhoudt bijvoorbeeld een repository met indicators of compromise, met lijsten van pakketten en versies.
- Doorzoek elke lockfile, op elke branch die tijdens de periode CI heeft gedraaid, niet alleen op de standaardbranch.
- Doorzoek de builds zelf. Zoek in CI-logs naar
setup_bun.js,bun_environment.jsof andere bestandsnamen uit het advies, en naar installatie-uitvoer waarin de getroffen versies staan. Bewaart u SBOM's per build, doorzoek die dan op de versies. - Controleer uw GitHub-organisatie op nieuwe repositories die naar de worm zijn genoemd, workflowbestanden die u niet hebt geschreven (bijvoorbeeld
shai-hulud-workflow.yml,discussion.yamlofformatter_*.yml) en self-hosted runners die u niet hebt geregistreerd.
Commando's die u kunt aanpassen. Om de lockfiles van elke remote branch te doorzoeken, zet u de getroffen versies van de golf in affected.txt, één name@version per regel:
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
Om de self-hosted runners van uw organisatie op te sommen, nieuwe repositories die de worm noemen, en de workflowbestanden waarvan bekend is dat de worm ze aanmaakt (vervang 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
Repositories kunnen ook eigen runners hebben (gh api repos/ORG/REPO/actions/runners), en vroege golven maakten repositories aan in de persoonlijke accounts van de ontwikkelaars van wie ze tokens hadden gestolen, dus vraag uw ontwikkelaars hun eigen accounts te controleren.
Waarom de lockfile het niet beslist
Het standaardadvies is om package-lock.json, yarn.lock of pnpm-lock.yaml te doorzoeken op de getroffen versies. Doe dat eerst, maar een schone lockfile bewijst geen schone build:
- Pakketten die buiten de lockfile om zijn geïnstalleerd.
npx some-toolinstalleert een pakket dat geen projectafhankelijkheid is in de npm-cache.npm install -gin een setupstap, een tool die in een buildscript wordt geïnstalleerd en eennpm installin een Dockerfile omzeilen allemaal de lockfile van het project. - De lockfile op de branch van vandaag is niet de lockfile die draaide. Een build van drie weken geleden gebruikte wat de lockfile toen zei, op de branch die de build startte. Pull requests voor afhankelijkheidsupdates draaien CI tegen nieuwe versies voordat iemand ze beoordeelt.
- Caches zetten terug wat eerder is geïnstalleerd. Een teruggezette
node_modulesof npm-cache kan een versie bevatten die de huidige lockfile niet meer noemt. npm ciinstalleert wat de lockfile zegt. Was de geïnfecteerde versie al vastgelegd, dan installeert een strikte installatie die braaf.- Een correct gepubliceerde versie kan toch kwaadaardig zijn. TanStack en Miasma werden gecompromitteerd via hun eigen releaseworkflows, en de geïnfecteerde versies van Miasma hadden geldige herkomstgegevens. Controleren of een versie correct is gepubliceerd, vertelt u niet dat die schoon is.
Wat u werkelijk nodig hebt, is een record van welke pakketversies elke build heeft opgehaald, over elke repository en elke branch, voor de hele periode dat een geïnfecteerde versie in het register stond. Zie zijn we getroffen? Zo beoordeelt u een nieuwe CVE over al uw builds.
Hoe u het oplost, en wat u doet als u nog niet kunt patchen
- Heeft een build een getroffen versie geïnstalleerd, behandel de job dan als gecompromitteerd. Roteer elk secret dat de job kon lezen: npm-tokens, GitHub-tokens, cloudinloggegevens en deploy keys. Omdat latere golven gestolen secrets in de accounts van andere slachtoffers publiceerden, moet u er niet van uitgaan dat u veilig bent omdat er in uw eigen account niets is verschenen.
- Maak de caches leeg. Verwijder GitHub Actions-caches en de npm-cache op machines van ontwikkelaars en self-hosted runners, zodat de volgende build schone versies installeert:
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
- Pin vast op een schone versie en verwijder de workflows, repositories, runners en configuratiebestanden die de worm heeft toegevoegd.
Het opruimen verifiëren
- Elke lockfile verwijst naar schone versies. Op elke branch toont
npm ls <package>(ofpnpm whyofyarn why) de geïnstalleerde versie; geen enkele mag op de lijst van de golf staan. - Een schone installatie bevat geen payloadbestanden. Installeer na het legen van de caches hierboven opnieuw en zoek naar de payloadbestanden die in de adviezen worden genoemd:
find node_modules -name bundle.js -size +3M \
-o -name setup_bun.js -o -name bun_environment.js -o -name router_init.js
- De oude inloggegevens werken niet meer. Ingetrokken npm-tokens staan niet meer in
npm token list, oude GitHub-tokens en deploy keys zijn verwijderd, en cloudsleutels zijn gedeactiveerd, niet alleen vervangen. - De caches zijn weg.
gh cache listtoont in geen enkele getroffen repository een cache die vóór het opruimen is aangemaakt. - Niets wat de worm heeft toegevoegd, is er nog: geen repositories, workflowbestanden, self-hosted runners of configuratiebestanden van editors en AI-assistenten die u niet hebt aangemaakt.
- De volgende build is schoon. Controleer de versies die hij daadwerkelijk heeft geïnstalleerd, aan de hand van zijn log of SBOM per build, niet alleen de lockfile.
Hoe u de schade de volgende keer beperkt
- Schakel installatiescripts uit waar dat kan. npm v12, uitgebracht in juli 2026, schakelt installatiescripts standaard uit en laat u ze per pakket toestaan. pnpm en Yarn bieden vergelijkbare opties.
- Wacht voordat u nieuwe versies overneemt. De meeste geïnfecteerde versies worden binnen uren of dagen gevonden en verwijderd. Een cooldown op afhankelijkheidsupdates, die zowel Dependabot als Renovate ondersteunen, houdt ze uit uw builds. De package firewall van GitHub, sinds 20 oktober 2026 in public preview voor GitHub Enterprise Cloud, controleert npm-installaties op kwaadaardige pakketten en biedt ook cooldowns.
- Publiceer met trusted publishing en kortlevende tokens. npm trok in december 2025 alle classic tokens in. Gebruik op OIDC gebaseerde trusted publishing vanuit CI in plaats van langlevende tokens, en houd publicatieworkflows gescheiden van build- en testworkflows.
- Beveilig de workflows die de worm misbruikt. Vermijd
pull_request_target, pin actions vast op commit-SHA's en geefGITHUB_TOKENalleen de rechten die elke job nodig heeft. Zie GitHub Actions-secrets: hoe ze lekken en hoe u ze afschermt. - Beperk waarmee builds verbinding kunnen maken. Een build heeft zijn pakketregisters en een paar API's nodig. Kan hij alleen die bereiken, dan mislukken de aanroepen van de worm naar
webhook.siteof zijn eigen servers. Zie hoe een egressbeleid exfiltratie uit GitHub Actions stopt. Dit stopt geen exfiltratie via een host die de build legitiem gebruikt, zoals GitHubs eigen API, waarop sommige golven steunden.
Controleren op compromittering
Shai-Hulud is kwaadaardig van opzet: elke machine of job die een geïnfecteerde versie installeerde, heeft hem uitgevoerd. De indicatoren verschillen per golf, dus gebruik het advies voor de golf die u controleert. Uit primaire bronnen:
| Golf | Indicatoren | Bron |
|---|---|---|
| September 2025 | Payload bundle.js, SHA-256 46faab8ab153fae6e80e7cca38eab363075bb524edd79e42269217a083628f09; workflow .github/workflows/shai-hulud-workflow.yml op een branch shai-hulud; een openbare repository met de naam Shai-Hulud; verbindingen met webhook.site | StepSecurity, CISA |
| November 2025 | setup_bun.js, SHA-1 d1829b4708126dcc7bea7437c04d1f10eacd4a16; bun_environment.js, SHA-1 d60ec97eea19fffb4809bc35b91033b52490ca11 of 3d7570d14d34b0ba137d502f042b27b0f37a59fa; repositories met cloud.json, contents.json, environment.json, truffleSecrets.json en actionsSecrets.json; workflows .github/workflows/discussion.yaml en formatter_*.yml; een self-hosted runner met de naam SHA1HULUD | Wiz |
| Mei 2026, TanStack | "@tanstack/setup": "github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c" in optionalDependencies; payload router_init.js (ongeveer 2,3 MB) in de root van het pakket; hulpscript tanstack_runner.js; verbindingen met filev2.getsession.org en seed1.getsession.org tot en met seed3.getsession.org, en downloads van de tweede fase vanaf litter.catbox.moe | GHSA-g7cv-rxg3-hmpx |
Latere golven veranderden de payload per infectie, dus een bestandshash die niet overeenkomt, bewijst niets; vergelijk eerst de pakketversies.
Wanneer u van compromittering moet uitgaan. Behandel elke host en elke CI-job die een getroffen versie installeerde als gecompromitteerd, ook als de installatie mislukte of niets in node_modules achterliet. Het TanStack-advies van GitHub zegt dat elke ontwikkel- of CI-omgeving die een getroffen versie installeerde, "als gecompromitteerd moet worden beschouwd." Controleer vanaf de eerste publicatie van de geïnfecteerde versie, niet vanaf de dag waarop de golf werd aangekondigd.
Roteer inloggegevens. Roteer alles wat de installatie kon bereiken. Bij de TanStack-golf ging het volgens de postmortem om inloggegevens voor AWS, GCP, Kubernetes, Vault, GitHub, npm en SSH. Controleer de auditlogs van uw cloud op activiteit van de getroffen hosts tijdens en na het installatievenster.
Verwijder caches en artefacten. Doorzoek artefactrepositories en dependency-proxy's op gecachte kopieën van de getroffen versies en verwijder ze, zoals CISA adviseert. Verwijder node_modules, de npm-cache en CI-caches; bij de TanStack-golf kwam de aanvaller binnen via een vergiftigde pnpm-storecache, dus controleer Actions-caches net zo goed als pakketcaches. Bouw en publiceer alles opnieuw wat een gecompromitteerde job heeft gemaakt.
Wat u uw klanten vertelt
Als u software levert die met npm is gebouwd, zullen klanten vragen of uw product de worm bevat of dat uw releasepipeline gecompromitteerd is. Antwoord met wat u hebt gecontroleerd en wat u hebt gedaan:
We hebben elke build van [product] tussen [datum] en [datum] vergeleken met de gepubliceerde lijsten van npm-pakketversies die met Shai-Hulud zijn geïnfecteerd. [Geen enkele build heeft een getroffen versie geïnstalleerd.] / [Builds van [component] op [datum] hebben [pakket@versie] geïnstalleerd; we hebben de inloggegevens geroteerd waar die builds bij konden, [product] opnieuw gebouwd met schone afhankelijkheden en [versie] uitgebracht. Releases [x tot y] zijn getroffen; upgrade naar [versie].] We werken deze melding bij als nieuwe golven het beeld veranderen.
Als u nog aan het controleren bent, zeg dat dan, en zeg wanneer u antwoordt. Een eerste verklaring en sjablonen voor elke uitkomst vindt u in hoe u klanten antwoordt na een aanval op de softwareketen.
Hoe CRACI helpt
CRACI draait uw GitHub Actions-jobs op eigen runners (runs-on: craci) en legt elk pakket vast dat elke job daadwerkelijk ophaalt uit npm en andere registers, inclusief pakketten die uit caches zijn teruggezet. Dat record dekt de bovenstaande blinde vlekken: npx-tools, globale installaties en installaties in buildscripts verschijnen allemaal, omdat het record uitgaat van wat de job heeft gedownload, niet van wat een lockfile declareerde. Wanneer een nieuwe golf wordt aangekondigd, zoekt u de getroffen versies op in de builds die CRACI heeft vastgelegd, in plaats van ze te reconstrueren uit lockfiles en logs, en de netwerktrace van elke job toont waarmee die verbinding maakte. De buildgeschiedenis bewaart elke eerdere build met zijn SBOM, zodat u binnen uw bewaartermijn ziet welke builds een geïnfecteerde versie bevatten en in welk tijdvenster u blootgesteld was.
Het egressbeleid van CRACI beperkt waarmee een job verbinding kan maken, wordt gevalideerd voordat de job start en valt veilig dicht, en waarschuwt u wanneer een job een bestemming probeert te bereiken die het beleid niet toestaat. Geïnfecteerde releases met een gepubliceerde CVE, zoals die van de TanStack-golf, worden gemarkeerd zoals elke andere kwetsbaarheid, en policy gates kunnen builds blokkeren die ze bevatten. CRACI beoordeelt pakketten niet op kwaadaardig gedrag en markeert een nieuwe geïnfecteerde versie niet voordat er een advies bestaat; combineer het met een tool voor pakketreputatie als u geïnfecteerde versies vóór de installatie wilt blokkeren.
Bekijk het pakketrecord van een van uw eigen builds
Andere incidenten: alle kwetsbaarheden.
Bronnen en updates
Primaire bronnen en origineel onderzoek:
- CISA: waarschuwing van 23 september 2025, KEV-catalogus
- GitHub: GHSA-g7cv-rxg3-hmpx (CVE-2026-45321), ons plan voor een veiligere npm-softwareketen, intrekking van de npm classic tokens
- Postmortem van TanStack
- Wiz over de golf van november 2025 en de nasleep ervan
- Microsoft over AntV en de detectierichtlijnen van Microsoft
- Datadog en Elastic over CHAINDROP
- StepSecurity over de golf van september 2025
- FIRST: EPSS-score voor CVE-2026-45321
Updates:
-
2026-11-11: EPSS, de CWE-klasse en de credit toegevoegd aan het overzichtsblok, plus controles om te kopiëren en secties over hoe de worm werkt, het verifiëren van het opruimen en het controleren op compromittering, met indicatoren per golf.
-
2026-11-11: Van het blog naar de sectie kwetsbaarheden verplaatst. Toegevoegd: de tensorlake-golf van 8 oktober 2026, de datum van de terugkeer in september 2026, CVE-2026-45321 en de KEV-vermelding ervan voor de TanStack-golf, en de package firewall van GitHub.
-
2026-09-25: Eerste publicatie op het CRACI-blog.