Shai-Hulud är en självspridande mask som infekterar npm-paket, en attack mot mjukvarans leveranskedja som värvar varje offer för att sprida den vidare. Den körs när ett paket installeras, stjäl npm-tokens, GitHub-tokens och molnnycklar från maskinen och använder npm-tokenen för att publicera infekterade versioner av offrets andra paket. Sedan den första vågen i september 2025 har den kommit tillbaka gång på gång, och i maj 2026 publicerade upphovsmännen källkoden, så varje våg sedan dess kan vara en annan grupp. Om något av dina byggen installerade en infekterad version ska du behandla det jobbet som komprometterat och rotera varje hemlighet det kunde läsa.
Den största skadan sker i CI. I vågen i november 2025 fann Wiz att bara 23 % av infektionerna fanns på utvecklares datorer, resten mestadels i CI/CD-pipelines, där GitHub Actions var vanligast. Ett bygge installerar hundratals paket, har hemligheterna som krävs för att publicera och driftsätta, och kör varje installationsskript utan att någon tittar på. Så när en ny våg tillkännages ställer varje team samma fråga: installerade något av våra byggen en infekterad version?
Vad som hände
| När | Våg | Vad som skilde sig |
|---|---|---|
| September 2025 | Shai-Hulud | Ett postinstall-skript körde en bundle.js på 3,6 MB, använde TruffleHog för att hitta hemligheter och publicerade om upp till 20 av underhållarens paket. Fler än 500 paket drabbades, däribland @ctrl/tinycolor och 18 @crowdstrike-paket. CISA publicerade en varning den 23 september. |
| November 2025 | Sha1-Hulud: The Second Coming | Flyttade till preinstall, installerade körmiljön Bun för att köra en nyttolast på 10 MB, försåg upp till 100 paket per underhållare med bakdörrar och registrerade infekterade maskiner som egenhostade GitHub-runners. Paket från Zapier, ENS, PostHog och Postman drabbades; Wiz räknade fler än 25 000 repon med läckta hemligheter. |
| December 2025 | ”3.0”-test | En modifierad version i ett enda paket, utan nämnvärd spridning. |
| Maj 2026 | TanStack | 84 skadliga versioner av 42 @tanstack-paket. Angriparen stal aldrig någon npm-token: ett pull_request_target-arbetsflöde, en förgiftad Actions-cache och en OIDC-token som lästes ur runnerns minne gjorde att angriparen kunde publicera via TanStacks egen trusted publishing. Spåras som CVE-2026-45321, tillagd i CISA:s KEV-katalog den 27 maj. |
| Maj 2026 | AntV | 639 versioner av 323 paket. På Linux-runners i GitHub Actions läste nyttolasten hemligheter ur runnerprocessens minne och kringgick loggmaskeringen. GitHub ogiltigförklarade 61 274 npm-skrivtokens. |
| Juni 2026 | Miasma | 32 @redhat-cloud-services-paket, publicerade via injicerade arbetsflöden med giltig proveniens. Varje infektion bar en unikt krypterad nyttolast, så hashbaserad upptäckt misslyckades. |
| Augusti 2026 | CHAINDROP | Populära cachepaket som keyv, cacheable och cache-manager, med totalt hundratals drabbade paket. Masken bet sig fast via konfigurationsfiler för editorer och AI-assistenter i repot. |
| September 2026 | Återkomst | Den 7 september kom nyttolasten tillbaka i fyra paket som tog sig förbi npms nya skanning efter skadlig kod vid publicering. |
| Oktober 2026 | tensorlake | Den 8 oktober publicerades [email protected] med masken. OX Security såg nya angriparnycklar och tror att det rör sig om ”en oberoende hotaktör eller en ny grupp som använder namnet Shai-Hulud”. |
Vad masken gör i ett CI-jobb
Detaljerna ändras mellan vågorna, men mönstret består:
- Den körs vid installation. Ett
preinstall- ellerpostinstall-skript körs så snartnpm installellernpm cinår den infekterade versionen. Inget i din kod behöver importera den. - Den samlar in hemligheter. Miljövariabler,
.npmrc-tokens, molnuppgifter från metadatatjänsten och hemlighetshanterare, och allt som TruffleHog hittar på disk. Senare vågor läste GitHub Actions-runnerns minne direkt, vilket ger hemligheter som aldrig exponerades för steget och som maskeringen skulle ha dolt i loggar. - Den för ut dem. Tidiga vågor skapade publika GitHub-repon uppkallade efter masken och pushade ett arbetsflöde som skickade varje repohemlighet till
webhook.site. Senare vågor använde angriparservrar och meddelandenätverk. - Den sprider sig och stannar kvar. Med en stulen npm-token publicerar den infekterade versioner av andra paket. Med en GitHub-token lägger den till arbetsflöden, registrerar egenhostade runners eller planterar konfigurationsfiler som körs nästa gång någon öppnar repot.
- Den beter sig annorlunda i CI. Nyttolasten från november 2025 letade efter
GITHUB_ACTIONS,BUILDKITE,CIRCLE_SHA1och liknande variabler, och i CI kördes den synkront och blev klar innan bygget gick vidare.
Vem som berörs
Alla vars maskiner eller byggen installerade en infekterad version medan den fanns i registret: utvecklares datorer, CI-jobb, Docker-byggen som kör npm install och egenhostade runners. Du behöver inte vara direkt beroende av paketet; ett transitivt beroende eller ett verktyg som körs med npx räcker. Varje våg har sin egen lista över paket och versioner, och infekterade versioner tas oftast bort inom timmar eller dagar, så det som spelar roll är om något installerade dem under det fönstret.
Så fungerar det
Shai-Hulud är ingen bugg i npm. Det är skadlig kod som publiceras inuti annars legitima paket, och som GitHubs säkerhetsmeddelanden om skadlig kod för de infekterade paketen klassar som inbäddad skadlig kod (CWE-506). Varje våg kombinerar några tekniker:
- Körning vid installation. npm kör ett pakets livscykelskript, som
preinstall,postinstallochprepare, under installationen, med användarens eller CI-jobbets behörigheter. En infekterad version behöver bara finnas i beroendeträdet; inget behöver importera den. - Spridning med stulna publiceringsrättigheter. Med en npm- eller GitHub-token som tagits från ett offer publicerar masken nya versioner av offrets andra paket med nyttolasten tillagd, så varje infekterad underhållare infekterar fler paket.
- Publicering via offrets egen release-pipeline (TanStack, maj 2026). Enligt GitHubs säkerhetsmeddelande kedjade angriparen ihop ett
pull_request_target-arbetsflöde, en GitHub Actions-cache som förgiftats över förtroendegränsen mellan fork och basrepo, och en OIDC-token som lästes ur runnerns minne, och publicerade via TanStacks trusted publishing utan att ändra publiceringsarbetsflödet. De infekterade versionerna deklarerade ett valfritt beroende på en commit i en fork, varsprepare-skript körde nyttolasten och sedan avsiktligt misslyckades, så att npm kastade beroendet och inga spår lämnades inode_modules. - Giltig proveniens (Miasma, juni 2026). Injicerade arbetsflöden publicerade från offrens egna repon, så de infekterade versionerna hade giltig proveniens, och en unikt krypterad nyttolast per infektion gick förbi hashbaserad upptäckt.
Så kontrollerar du om du berörs
- Hämta listan över berörda versioner för vågen. Säkerhetsleverantörer publicerar dem inom några timmar; Datadog har till exempel ett repo med indikatorer på kompromettering med paket- och versionslistor.
- Sök i varje lockfil, på varje gren som körde CI under perioden, inte bara standardgrenen.
- Sök i själva byggena. Leta i CI-loggar efter
setup_bun.js,bun_environment.jseller andra filnamn som nämns i säkerhetsmeddelandet, och efter installationsutdata som nämner de berörda versionerna. Om du sparar SBOM:er per bygge, sök efter versionerna i dem. - Kontrollera din GitHub-organisation efter nya repon uppkallade efter masken, arbetsflödesfiler du inte har skrivit (till exempel
shai-hulud-workflow.yml,discussion.yamlellerformatter_*.yml) och egenhostade runners du inte har registrerat.
Kommandon du kan anpassa. För att söka i lockfilerna på varje fjärrgren lägger du vågens berörda versioner i affected.txt, en name@version per rad:
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
För att lista organisationens egenhostade runners, nya repon som nämner masken och de arbetsflödesfiler som masken är känd för att skapa (ersätt 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
Repon kan också ha egna runners (gh api repos/ORG/REPO/actions/runners), och tidiga vågor skapade repon på de personliga kontona för de utvecklare vars tokens de stal, så be dina utvecklare att kontrollera sina.
Därför avgör inte lockfilen saken
Standardrådet är att söka efter de berörda versionerna i package-lock.json, yarn.lock eller pnpm-lock.yaml. Gör det först, men en ren lockfil bevisar inte ett rent bygge:
- Paket som installeras utanför lockfilen.
npx some-toolinstallerar ett paket som inte är ett projektberoende i npm-cachen.npm install -gi ett installationssteg, ett verktyg som installeras i ett byggskript och ennpm installi en Dockerfile går alla förbi projektets lockfil. - Lockfilen på grenen i dag är inte lockfilen som kördes. Ett bygge för tre veckor sedan använde det som lockfilen sa då, på den gren som utlöste det. Pull requests med beroendeuppdateringar kör CI mot nya versioner innan någon granskar dem.
- Cachar återställer det som installerades tidigare. En återställd
node_moduleseller npm-cache kan innehålla en version som den aktuella lockfilen inte längre nämner. npm ciinstallerar det lockfilen säger. Om den infekterade versionen redan var låst installerar en strikt installation den troget.- En korrekt publicerad version kan ändå vara skadlig. TanStack och Miasma komprometterades via sina egna release-arbetsflöden, och Miasmas infekterade versioner hade giltig proveniens. Att en version publicerades korrekt säger inte att den är ren.
Det du faktiskt behöver är en registrering av vilka paketversioner varje bygge hämtade, över varje repo och varje gren, under hela perioden en infekterad version fanns i registret. Se är vi drabbade? så bedömer du en ny CVE över alla dina byggen.
Så åtgärdar du det, och vad du gör om du inte kan patcha än
- Om något bygge installerade en berörd version, behandla jobbet som komprometterat. Rotera varje hemlighet det kunde läsa: npm-tokens, GitHub-tokens, molnuppgifter och driftsättningsnycklar. Eftersom senare vågor publicerade stulna hemligheter på andra offers konton ska du inte utgå från att du är säker bara för att inget dök upp på dina egna.
- Rensa cacharna. Ta bort GitHub Actions-cachar och npm-cachen på utvecklardatorer och egenhostade runners, så att nästa bygge installerar rena 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
- Lås till en ren version och ta bort de arbetsflöden, repon, runners och konfigurationsfiler som masken lade till.
Verifiera saneringen
- Varje lockfil löser upp till rena versioner. På varje gren visar
npm ls <package>(ellerpnpm whyelleryarn why) vilken version som är installerad; ingen får finnas på vågens lista. - En ren installation innehåller inga nyttolastfiler. Efter cacherensningen ovan installerar du igen och letar efter de nyttolastfiler som nämns i säkerhetsmeddelandena:
find node_modules -name bundle.js -size +3M \
-o -name setup_bun.js -o -name bun_environment.js -o -name router_init.js
- De gamla inloggningsuppgifterna fungerar inte längre. Återkallade npm-tokens finns inte längre i
npm token list, gamla GitHub-tokens och driftsättningsnycklar är borttagna, och molnnycklar är inaktiverade, inte bara ersatta. - Cacharna är borta.
gh cache listvisar ingen cache som skapats före saneringen i något berört repo. - Inget som masken lade till finns kvar: inga repon, arbetsflödesfiler, egenhostade runners eller konfigurationsfiler för editorer och AI-assistenter som du inte har skapat.
- Nästa bygge är rent. Kontrollera vilka versioner det faktiskt installerade, i dess logg eller SBOM per bygge, inte bara i lockfilen.
Så minskar du skadan nästa gång
- Stäng av installationsskript där du kan. npm v12, som släpptes i juli 2026, inaktiverar installationsskript som standard och låter dig tillåta dem per paket. pnpm och Yarn har liknande kontroller.
- Vänta innan du tar in nya versioner. De flesta infekterade versioner hittas och tas bort inom timmar eller dagar. En karenstid för beroendeuppdateringar, som både Dependabot och Renovate stöder, håller dem borta från dina byggen. GitHubs paketbrandvägg, i offentlig förhandsversion för GitHub Enterprise Cloud sedan den 20 oktober 2026, kontrollerar npm-installationer efter skadliga paket och erbjuder också karenstider.
- Publicera med trusted publishing och kortlivade tokens. npm återkallade alla klassiska tokens i december 2025. Använd OIDC-baserad trusted publishing från CI i stället för långlivade tokens, och håll publiceringsarbetsflöden åtskilda från bygg- och testarbetsflöden.
- Härda arbetsflödena som masken missbrukar. Undvik
pull_request_target, lås actions till commit-SHA:er och geGITHUB_TOKENbara de behörigheter varje jobb behöver. Se hemligheter i GitHub Actions: så läcker de och så låser du dem. - Begränsa vart byggen kan ansluta. Ett bygge behöver sina paketregister och några API:er. Om det bara kan nå dem misslyckas maskens anrop till
webhook.siteeller till dess egna servrar. Se hur en egress-policy stoppar exfiltrering från GitHub Actions. Det stoppar inte exfiltrering via en värd som bygget legitimt använder, som GitHubs eget API, vilket vissa vågor förlitade sig på.
Kontrollera om du har komprometterats
Shai-Hulud är skadlig per design: varje maskin eller jobb som installerade en infekterad version körde den. Indikatorerna skiljer sig mellan vågorna, så använd säkerhetsmeddelandet för den våg du kontrollerar. Från primärkällor:
| Våg | Indikatorer | Källa |
|---|---|---|
| September 2025 | Nyttolasten bundle.js, SHA-256 46faab8ab153fae6e80e7cca38eab363075bb524edd79e42269217a083628f09; arbetsflödet .github/workflows/shai-hulud-workflow.yml på en gren shai-hulud; ett publikt repo med namnet Shai-Hulud; anslutningar till webhook.site | StepSecurity, CISA |
| November 2025 | setup_bun.js, SHA-1 d1829b4708126dcc7bea7437c04d1f10eacd4a16; bun_environment.js, SHA-1 d60ec97eea19fffb4809bc35b91033b52490ca11 eller 3d7570d14d34b0ba137d502f042b27b0f37a59fa; repon med cloud.json, contents.json, environment.json, truffleSecrets.json och actionsSecrets.json; arbetsflödena .github/workflows/discussion.yaml och formatter_*.yml; en egenhostad runner med namnet SHA1HULUD | Wiz |
| Maj 2026, TanStack | "@tanstack/setup": "github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c" i optionalDependencies; nyttolasten router_init.js (cirka 2,3 MB) i paketets rot; hjälpskriptet tanstack_runner.js; anslutningar till filev2.getsession.org och seed1.getsession.org till seed3.getsession.org, och nedladdningar i andra steget från litter.catbox.moe | GHSA-g7cv-rxg3-hmpx |
Senare vågor ändrade nyttolasten för varje infektion, så en filhash som inte matchar bevisar ingenting; matcha först på paketversioner.
När du ska utgå från att du har komprometterats. Behandla varje värd och CI-jobb som installerade en berörd version som komprometterad, även om installationen misslyckades eller inte lämnade något i node_modules. GitHubs säkerhetsmeddelande om TanStack säger att varje utvecklar- eller CI-miljö som installerade en berörd version ”bör betraktas som komprometterad.” Kontrollera från den första publiceringen av den infekterade versionen, inte från dagen då vågen tillkännagavs.
Rotera inloggningsuppgifter. Rotera allt som installationen kunde nå. I TanStack-vågen innebar det inloggningsuppgifter för AWS, GCP, Kubernetes, Vault, GitHub, npm och SSH, enligt postmortem-rapporten. Granska molnets granskningsloggar efter aktivitet från de berörda värdarna under och efter installationsfönstret.
Rensa cachar och artefakter. Sök i artefaktrepon och beroendeproxyer efter cachade kopior av de berörda versionerna och ta bort dem, som CISA rekommenderar. Ta bort node_modules, npm-cachen och CI-cachar; i TanStack-vågen tog sig angriparen in via en förgiftad cache för pnpm-lagret, så kontrollera Actions-cachar såväl som paketcachar. Bygg om och släpp på nytt allt som ett komprometterat jobb producerade.
Vad du ska säga till dina kunder
Om du levererar programvara som byggs med npm kommer kunderna att fråga om din produkt bär på masken eller om din release-pipeline har komprometterats. Svara med vad du kontrollerade och vad du gjorde:
Vi har kontrollerat varje bygge av [produkt] mellan [datum] och [datum] mot de publicerade listorna över npm-paketversioner infekterade med Shai-Hulud. [Inget bygge installerade en berörd version.] / [Byggen av [komponent] den [datum] installerade [paket@version]; vi har roterat de inloggningsuppgifter som de byggena hade åtkomst till, byggt om [produkt] från rena beroenden och släppt [version]. Release [x till y] berörs; uppgradera till [version].] Vi uppdaterar det här meddelandet om nya vågor förändrar läget.
Om du fortfarande kontrollerar, säg det, och säg när du kommer att svara. Ett första uttalande och mallar för varje utfall finns i hur du svarar kunder efter en attack mot leveranskedjan.
Så hjälper CRACI
CRACI kör dina GitHub Actions-jobb på sina egna runners (runs-on: craci) och registrerar varje paket som varje jobb faktiskt hämtar från npm och andra register, inklusive paket som återställs från cachar. Den registreringen täcker de blinda fläckarna ovan: npx-verktyg, globala installationer och installationer i byggskript syns alla, eftersom registreringen kommer från vad jobbet laddade ner, inte från vad en lockfil deklarerade. När en ny våg tillkännages slår du upp de berörda versionerna i de byggen CRACI har registrerat i stället för att rekonstruera dem från lockfiler och loggar, och varje jobbs nätverksspårning visar vart det anslöt. Bygghistoriken sparar varje tidigare bygge med dess SBOM, så att du ser vilka byggen som innehöll en infekterad version och under vilket fönster du var exponerad, inom din lagringsperiod.
CRACI:s egress-policy begränsar vart ett jobb kan ansluta, valideras innan jobbet startar, blockerar vid fel och larmar dig när ett jobb försöker nå en destination som policyn inte tillåter. Infekterade releaser med en publicerad CVE, som TanStack-vågen, flaggas som vilken annan sårbarhet som helst, och policyspärrar kan blockera byggen som innehåller dem. CRACI poängsätter inte paket utifrån skadligt beteende och flaggar inte en ny infekterad version innan ett säkerhetsmeddelande finns; kombinera det med ett verktyg för paketrykte om du vill att infekterade versioner ska blockeras före installation.
Se paketregistreringen för ett av dina egna byggen
Andra incidenter: alla sårbarheter.
Källor och uppdateringar
Primärkällor och originalforskning:
- CISA: varning av den 23 september 2025, KEV-katalogen
- GitHub: GHSA-g7cv-rxg3-hmpx (CVE-2026-45321), vår plan för en säkrare leveranskedja i npm, npm:s klassiska tokens återkallade
- TanStacks postmortem
- Wiz om vågen i november 2025 och efterspelet
- Microsoft om AntV och Microsofts vägledning för upptäckt
- Datadog och Elastic om CHAINDROP
- StepSecurity om vågen i september 2025
- FIRST: EPSS-poäng för CVE-2026-45321
Uppdateringar:
- 2026-11-11: Lade till EPSS, CWE-klassen och krediteringen i rutan, kontroller att kopiera och klistra in, samt avsnitt om hur masken fungerar, hur du verifierar saneringen och hur du kontrollerar om du har komprometterats, med indikatorer per våg.
- 2026-11-11: Flyttad från bloggen till avsnittet om sårbarheter. Lade till vågen med tensorlake den 8 oktober 2026, datumet för återkomsten i september 2026, CVE-2026-45321 och dess KEV-listning för TanStack-vågen samt GitHubs paketbrandvägg.
- 2026-09-25: Först publicerad på CRACI-bloggen.