Shai-Hulud er en selvspredende orm, der inficerer npm-pakker, et angreb på softwareforsyningskæden, der hverver hvert offer til at sprede det videre. Den kører, når en pakke installeres, stjæler npm-tokens, GitHub-tokens og cloud-nøgler fra maskinen og bruger npm-tokenet til at udgive inficerede versioner af ofrets andre pakker. Siden den første bølge i september 2025 er den vendt tilbage gentagne gange, og i maj 2026 offentliggjorde ophavsmændene kildekoden, så hver bølge siden da kan stamme fra en anden gruppe. Hvis nogen af dine builds installerede en inficeret version, så behandl det job som kompromitteret, og rotér hver secret, det kunne læse.
Det meste af skaden sker i CI. I bølgen i november 2025 fandt Wiz, at kun 23 % af infektionerne var på udvikleres maskiner, resten hovedsageligt i CI/CD-pipelines, oftest GitHub Actions. Et build installerer hundredvis af pakker, indeholder de secrets, der skal til for at udgive og deploye, og kører hvert install-script, uden at nogen holder øje. Så når en ny bølge bliver annonceret, stiller hvert team det samme spørgsmål: Installerede nogen af vores builds en inficeret version?
Hvad der skete
| Hvornår | Bølge | Hvad der var anderledes |
|---|---|---|
| September 2025 | Shai-Hulud | Et postinstall-script kørte en bundle.js på 3,6 MB, brugte TruffleHog til at finde secrets og genudgav op til 20 af maintainerens pakker. Mere end 500 pakker blev ramt, inklusive @ctrl/tinycolor og 18 @crowdstrike-pakker. CISA udsendte en advarsel den 23. september. |
| November 2025 | Sha1-Hulud: The Second Coming | Flyttede til preinstall, installerede Bun-runtimen for at køre en payload på 10 MB, indsatte bagdøre i op til 100 pakker pr. maintainer og registrerede inficerede maskiner som selvhostede GitHub-runnere. Pakker fra Zapier, ENS, PostHog og Postman blev ramt, og Wiz talte mere end 25.000 repositories med lækkede secrets. |
| December 2025 | "3.0"-test | En ændret version i en enkelt pakke, uden nævneværdig spredning. |
| Maj 2026 | TanStack | 84 ondsindede versioner af 42 @tanstack-pakker. Angriberen stjal aldrig et npm-token: Et pull_request_target-workflow, en forgiftet Actions-cache og et OIDC-token læst fra runnerens hukommelse gjorde det muligt at udgive gennem TanStacks egen trusted publishing. Registreret som CVE-2026-45321 og føjet til CISA's KEV-katalog den 27. maj. |
| Maj 2026 | AntV | 639 versioner af 323 pakker. På GitHub Actions' Linux-runnere læste payloaden secrets fra runner-processens hukommelse og omgik dermed logmaskering. GitHub ugyldiggjorde 61.274 npm-skrivetokens. |
| Juni 2026 | Miasma | 32 @redhat-cloud-services-pakker, udgivet via injicerede workflows med gyldig provenance. Hver infektion bar en unikt krypteret payload, så hash-baseret detektion fejlede. |
| August 2026 | CHAINDROP | Populære caching-pakker som keyv, cacheable og cache-manager, med hundredvis af ramte pakker i alt. Den bevarede fodfæstet gennem konfigurationsfiler til editorer og AI-assistenter i repositoriet. |
| September 2026 | Genkomst | Den 7. september vendte payloaden tilbage i fire pakker, der slap forbi npm's nye malware-scanning ved udgivelse. |
| Oktober 2026 | tensorlake | Den 8. oktober blev [email protected] udgivet med ormen. OX Security så nye angribernøgler og peger på "en uafhængig trusselsaktør eller en ny gruppe, der bruger navnet Shai-Hulud". |
Hvad ormen gør i et CI-job
Detaljerne ændrer sig mellem bølgerne, men mønsteret holder:
- Den kører ved installation. Et
preinstall- ellerpostinstall-script kører, så snartnpm installellernpm cinår den inficerede version. Intet i din kode behøver at importere den. - Den indsamler secrets. Miljøvariabler,
.npmrc-tokens, cloud-legitimationsoplysninger fra metadatatjenesten og secret managers samt alt, hvad TruffleHog finder på disken. Senere bølger læste GitHub Actions-runnerens hukommelse direkte, hvilket giver secrets, som aldrig blev eksponeret for trinnet, og som maskering ville have skjult i logs. - Den får dem ud. Tidlige bølger oprettede offentlige GitHub-repositories opkaldt efter ormen og pushede et workflow, der sendte hver repository-secret til
webhook.site. Senere bølger brugte angribernes servere og beskednetværk. - Den spreder sig og bliver. Med et stjålet npm-token udgiver den inficerede versioner af andre pakker. Med et GitHub-token tilføjer den workflows, registrerer selvhostede runnere eller planter konfigurationsfiler, der kører, næste gang nogen åbner repositoriet.
- Den opfører sig anderledes i CI. Payloaden fra november 2025 tjekkede efter
GITHUB_ACTIONS,BUILDKITE,CIRCLE_SHA1og lignende variabler, og i CI kørte den synkront og blev færdig med sit arbejde, før buildet fortsatte.
Hvem er berørt
Alle, hvis maskiner eller builds installerede en inficeret version, mens den lå i registret: udvikleres bærbare, CI-jobs, Docker-builds, der kører npm install, og selvhostede runnere. Du behøver ikke at være direkte afhængig af pakken; en transitiv afhængighed eller et værktøj, der køres med npx, er nok. Hver bølge har sin egen liste over pakker og versioner, og inficerede versioner bliver som regel fjernet inden for timer eller dage, så det afgørende er, om noget installerede dem i det tidsvindue.
Sådan virker det
Shai-Hulud er ikke en fejl i npm. Det er ondsindet kode, der udgives inde i ellers legitime pakker, og som GitHubs malware-advisories for de inficerede pakker klassificerer som indlejret ondsindet kode (CWE-506). Hver bølge kombinerer nogle få teknikker:
- Kørsel ved installation. npm kører en pakkes lifecycle-scripts, såsom
preinstall,postinstallogprepare, under installationen med brugerens eller CI-jobbets rettigheder. En inficeret version skal blot være i afhængighedstræet; intet behøver at importere den. - Spredning med stjålne udgivelsesrettigheder. Med et npm- eller GitHub-token taget fra et offer udgiver ormen nye versioner af ofrets andre pakker med payloaden tilføjet, så hver inficeret maintainer inficerer flere pakker.
- Udgivelse gennem ofrets egen release-pipeline (TanStack, maj 2026). Ifølge GitHubs advisory kædede angriberen et
pull_request_target-workflow, en GitHub Actions-cache forgiftet på tværs af tillidsgrænsen mellem fork og base-repository og et OIDC-token læst fra runnerens hukommelse sammen og udgav gennem TanStacks trusted publishing uden at ændre publish-workflowet. De inficerede versioner erklærede en valgfri afhængighed af et commit i en fork, hvisprepare-script kørte payloaden og derefter fejlede med vilje, så npm kasserede afhængigheden og ikke efterlod spor inode_modules. - Gyldig provenance (Miasma, juni 2026). Injicerede workflows udgav fra ofrenes egne repositories, så de inficerede versioner havde gyldig provenance, og en unikt krypteret payload pr. infektion slog hash-baseret detektion ud.
Sådan tjekker du, om du er berørt
- Hent listen over berørte versioner for bølgen. Sikkerhedsleverandører offentliggør dem inden for få timer. Datadog vedligeholder for eksempel et repository med indicators of compromise med pakke- og versionslister.
- Søg i hver lockfile på hver branch, der kørte CI i perioden, ikke kun default-branchen.
- Søg i selve buildene. Kig i CI-logs efter
setup_bun.js,bun_environment.jseller andre filnavne, der nævnes i advisoryen, og efter install-output, der nævner de berørte versioner. Hvis du gemmer SBOM'er pr. build, så søg efter versionerne i dem. - Tjek din GitHub-organisation for nye repositories opkaldt efter ormen, workflow-filer, du ikke har skrevet (for eksempel
shai-hulud-workflow.yml,discussion.yamlellerformatter_*.yml), og selvhostede runnere, du ikke har registreret.
Kommandoer, du kan tilpasse. For at søge i lockfiles på hver remote branch lægger du bølgens berørte versioner i affected.txt, én name@version pr. linje:
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
For at liste din organisations selvhostede runnere, nye repositories, der nævner ormen, og de workflow-filer, ormen vides at oprette (erstat 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 kan også have deres egne runnere (gh api repos/ORG/REPO/actions/runners), og tidlige bølger oprettede repositories på de personlige konti hos de udviklere, hvis tokens de stjal, så bed dine udviklere om at tjekke deres.
Derfor afgør lockfilen det ikke
Det almindelige råd er at søge efter de berørte versioner i package-lock.json, yarn.lock eller pnpm-lock.yaml. Gør det først, men en ren lockfile beviser ikke et rent build:
- Pakker installeret uden om lockfilen.
npx some-toolinstallerer en pakke, der ikke er en projektafhængighed, i npm-cachen.npm install -gi et opsætningstrin, et værktøj installeret i et build-script og ennpm installi en Dockerfile går alle uden om projektets lockfile. - Lockfilen på branchen i dag er ikke den lockfile, der kørte. Et build for tre uger siden brugte det, lockfilen sagde dengang, på den branch, der udløste det. Pull requests med afhængighedsopdateringer kører CI mod nye versioner, før nogen har gennemgået dem.
- Caches gendanner det, der blev installeret før. En gendannet
node_moduleseller npm-cache kan indeholde en version, den aktuelle lockfile ikke længere nævner. npm ciinstallerer det, lockfilen siger. Hvis den inficerede version allerede var låst, installerer en streng installation den trofast.- En korrekt udgivet version kan stadig være ondsindet. TanStack og Miasma blev kompromitteret gennem deres egne release-workflows, og Miasmas inficerede versioner havde gyldig provenance. At tjekke, at en version er udgivet korrekt, fortæller dig ikke, at den er ren.
Det, du faktisk har brug for, er en registrering af, hvilke pakkeversioner hvert build hentede, på tværs af hvert repository og hver branch, i hele den periode, en inficeret version lå i registret. Se er vi berørt? sådan vurderer du en ny CVE på tværs af dine builds.
Sådan retter du det, og hvad du gør, hvis du ikke kan patche endnu
- Hvis et build installerede en berørt version, så behandl jobbet som kompromitteret. Rotér hver secret, det kunne læse: npm-tokens, GitHub-tokens, cloud-legitimationsoplysninger og deploy-nøgler. Fordi senere bølger offentliggjorde stjålne secrets på andre ofres konti, skal du ikke gå ud fra, at du er sikker, fordi intet dukkede op på din egen.
- Ryd cachene. Slet GitHub Actions-caches og npm-cachen på udvikleres maskiner og selvhostede runnere, så næste build installerer rene versioner:
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
- Fastlås en ren version, og fjern de workflows, repositories, runnere og konfigurationsfiler, som ormen tilføjede.
Verificér oprydningen
- Hver lockfile peger på rene versioner. På hver branch viser
npm ls <package>(ellerpnpm whyelleryarn why) den installerede version; ingen må stå på bølgens liste. - En ren installation indeholder ingen payload-filer. Efter cache-rydningen ovenfor installerer du igen og kigger efter de payload-filer, der nævnes i advisories:
find node_modules -name bundle.js -size +3M \
-o -name setup_bun.js -o -name bun_environment.js -o -name router_init.js
- De gamle legitimationsoplysninger virker ikke længere. Tilbagekaldte npm-tokens er væk fra
npm token list, gamle GitHub-tokens og deploy-nøgler er slettet, og cloud-nøgler er deaktiveret, ikke kun erstattet. - Cachene er væk.
gh cache listviser ingen cache oprettet før oprydningen i noget berørt repository. - Intet, ormen tilføjede, er tilbage: ingen repositories, workflow-filer, selvhostede runnere eller konfigurationsfiler til editorer og AI-assistenter, du ikke har oprettet.
- Det næste build er rent. Tjek de versioner, det faktisk installerede, ud fra dets log eller SBOM pr. build, ikke kun lockfilen.
Sådan mindsker du skaden næste gang
- Slå install-scripts fra, hvor du kan. npm v12, der blev udgivet i juli 2026, slår som standard install-scripts fra og lader dig tillade dem pr. pakke. pnpm og Yarn har lignende kontroller.
- Vent, før du tager nye versioner i brug. De fleste inficerede versioner bliver fundet og fjernet inden for timer eller dage. En cooldown på afhængighedsopdateringer, som både Dependabot og Renovate understøtter, holder dem ude af dine builds. GitHubs package firewall, der har været i public preview for GitHub Enterprise Cloud siden den 20. oktober 2026, tjekker npm-installationer for ondsindede pakker og tilbyder også cooldowns.
- Udgiv med trusted publishing og kortlivede tokens. npm tilbagekaldte alle classic tokens i december 2025. Brug OIDC-baseret trusted publishing fra CI i stedet for langlivede tokens, og hold publish-workflows adskilt fra build- og test-workflows.
- Hærd de workflows, ormen misbruger. Undgå
pull_request_target, fastlås actions på commit-SHA'er, og givGITHUB_TOKENkun de rettigheder, hvert job har brug for. Se GitHub Actions-secrets: sådan lækker de, og sådan låser du dem ned. - Begræns, hvad builds kan oprette forbindelse til. Et build har brug for sine pakkeregistre og nogle få API'er. Hvis det kun kan nå dem, fejler ormens kald til
webhook.siteeller dens egne servere. Se hvordan en egress-politik stopper eksfiltrering fra GitHub Actions. Det stopper ikke eksfiltrering gennem en host, buildet bruger legitimt, for eksempel GitHubs eget API, som nogle bølger udnyttede.
Tjek for kompromittering
Shai-Hulud er ondsindet af design: Enhver maskine eller ethvert job, der installerede en inficeret version, kørte den. Indikatorerne er forskellige fra bølge til bølge, så brug advisoryen for den bølge, du tjekker. Fra primære kilder:
| Bølge | Indikatorer | Kilde |
|---|---|---|
| September 2025 | Payloaden bundle.js, SHA-256 46faab8ab153fae6e80e7cca38eab363075bb524edd79e42269217a083628f09; workflowet .github/workflows/shai-hulud-workflow.yml på en branch shai-hulud; et offentligt repository ved navn Shai-Hulud; forbindelser til webhook.site | StepSecurity, CISA |
| November 2025 | setup_bun.js, SHA-1 d1829b4708126dcc7bea7437c04d1f10eacd4a16; bun_environment.js, SHA-1 d60ec97eea19fffb4809bc35b91033b52490ca11 eller 3d7570d14d34b0ba137d502f042b27b0f37a59fa; repositories med cloud.json, contents.json, environment.json, truffleSecrets.json og actionsSecrets.json; workflows .github/workflows/discussion.yaml og formatter_*.yml; en selvhostet runner ved navn SHA1HULUD | Wiz |
| Maj 2026, TanStack | "@tanstack/setup": "github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c" i optionalDependencies; payloaden router_init.js (cirka 2,3 MB) i pakkens rod; hjælpescriptet tanstack_runner.js; forbindelser til filev2.getsession.org og seed1.getsession.org til seed3.getsession.org samt downloads i andet trin fra litter.catbox.moe | GHSA-g7cv-rxg3-hmpx |
Senere bølger ændrede payloaden for hver infektion, så en fil-hash, der ikke matcher, beviser intet; match først på pakkeversioner.
Hvornår du skal gå ud fra, at der er sket en kompromittering. Behandl hver host og hvert CI-job, der installerede en berørt version, som kompromitteret, også selvom installationen fejlede eller ikke efterlod noget i node_modules. GitHubs advisory om TanStack siger, at ethvert udvikler- eller CI-miljø, der installerede en berørt version, "bør betragtes som kompromitteret." Tjek fra den første udgivelse af den inficerede version, ikke fra den dag, bølgen blev annonceret.
Rotér legitimationsoplysninger. Rotér alt, hvad installationen kunne nå. I TanStack-bølgen betød det legitimationsoplysninger til AWS, GCP, Kubernetes, Vault, GitHub, npm og SSH ifølge postmortem-rapporten. Gennemgå cloud-audit-logs for aktivitet fra de berørte hosts under og efter installationsvinduet.
Ryd caches og artefakter. Søg i artefakt-repositories og afhængighedsproxyer efter cachede kopier af de berørte versioner, og slet dem, som CISA anbefaler. Slet node_modules, npm-cachen og CI-caches; i TanStack-bølgen kom angriberen ind via en forgiftet cache af pnpm-storen, så tjek Actions-caches såvel som pakke-caches. Genbyg og genudgiv alt, hvad et kompromitteret job har produceret.
Hvad du skal sige til dine kunder
Hvis du leverer software bygget med npm, vil kunderne spørge, om dit produkt bærer ormen, eller om din release-pipeline er blevet kompromitteret. Svar med, hvad du tjekkede, og hvad du gjorde:
Vi har tjekket hvert build af [produkt] mellem [dato] og [dato] mod de offentliggjorte lister over npm-pakkeversioner inficeret med Shai-Hulud. [Intet build installerede en berørt version.] / [Builds af [komponent] den [dato] installerede [pakke@version]; vi har roteret de legitimationsoplysninger, disse builds havde adgang til, genbygget [produkt] fra rene afhængigheder og udgivet [version]. Release [x til y] er berørt; opgradér venligst til [version].] Vi opdaterer denne meddelelse, hvis nye bølger ændrer billedet.
Hvis du stadig er ved at tjekke, så sig det, og sig, hvornår du svarer. En første udtalelse og skabeloner til hvert udfald finder du i sådan svarer du kunder efter et forsyningskædeangreb.
Sådan hjælper CRACI
CRACI kører dine GitHub Actions-jobs på sine egne runnere (runs-on: craci) og registrerer hver pakke, hvert job faktisk henter fra npm og andre registre, inklusive pakker gendannet fra caches. Den registrering dækker de blinde vinkler ovenfor: npx-værktøjer, globale installationer og installationer i build-scripts dukker alle op, fordi registreringen kommer fra det, jobbet downloadede, ikke fra det, en lockfile deklarerede. Når en ny bølge bliver annonceret, slår du de berørte versioner op i de builds, CRACI har registreret, i stedet for at rekonstruere dem ud fra lockfiles og logs, og hvert jobs netværkstrace viser, hvad det oprettede forbindelse til. Build-historikken gemmer hvert tidligere build med dets SBOM, så du ser, hvilke builds der indeholdt en inficeret version, og i hvilket tidsvindue du var eksponeret, inden for din opbevaringsperiode.
CRACI's egress-politik begrænser, hvad et job kan oprette forbindelse til, valideres, før jobbet starter, blokerer ved fejl og giver dig en alarm, når et job forsøger at nå en destination, politikken ikke tillader. Inficerede releases med en offentliggjort CVE, som TanStack-bølgen, bliver markeret som enhver anden sårbarhed, og policy gates kan blokere builds, der indeholder dem. CRACI vurderer ikke pakker for ondsindet adfærd og markerer ikke en ny inficeret version, før der findes en advisory; kombinér det med et værktøj til pakkeomdømme, hvis du vil have inficerede versioner blokeret før installation.
Se pakkeregistreringen af et af dine egne builds
Andre hændelser: alle sårbarheder.
Kilder og opdateringer
Primære kilder og original research:
- CISA: advarsel af 23. september 2025, KEV-kataloget
- GitHub: GHSA-g7cv-rxg3-hmpx (CVE-2026-45321), vores plan for en mere sikker npm-forsyningskæde, npm's classic tokens tilbagekaldt
- TanStacks postmortem
- Wiz om bølgen i november 2025 og eftervirkningerne
- Microsoft om AntV og Microsofts vejledning i detektion
- Datadog og Elastic om CHAINDROP
- StepSecurity om bølgen i september 2025
- FIRST: EPSS-score for CVE-2026-45321
Opdateringer:
- 2026-11-11: Tilføjet EPSS, CWE-klassen og krediteringen i boksen, tjek, du kan kopiere og indsætte, samt afsnit om, hvordan ormen virker, hvordan du verificerer oprydningen, og hvordan du tjekker for kompromittering, med indikatorer pr. bølge.
- 2026-11-11: Flyttet fra bloggen til sårbarhedssektionen. Tilføjet tensorlake-bølgen den 8. oktober 2026, datoen for genkomsten i september 2026, CVE-2026-45321 og dens KEV-optagelse for TanStack-bølgen samt GitHubs package firewall.
- 2026-09-25: Først udgivet på CRACI-bloggen.