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.
Den största skadan sker i CI. I vågen i november 2025 fann Wiz att bara 23 % av infektionerna fanns på utvecklares datorer: de övriga 77 % fanns 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å.
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? Det här inlägget går igenom vad masken gör, varför det vanliga svaret (”kolla lockfilen”) är ofullständigt och hur du besvarar frågan ordentligt.
Vågorna hittills
| När | Namn | 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. |
| 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. |
| 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. |
Nyttolasten kom tillbaka i september 2026, i fyra paket som tog sig förbi npms nya skanning efter skadlig kod vid publicering.
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.
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.
Så kontrollerar du om du drabbades
- 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. - Rensa cacharna. Ta bort GitHub Actions-cachar och npm-cachen på utvecklardatorer och egenhostade runners, så att nästa bygge installerar rena versioner.
- 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.
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.
- 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å.
Där CRACI passar in
CRACI kör dina GitHub Actions-jobb på sina egna runners (runs-on: craci), och en paketmedveten proxy på runnern 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. Fynd sammanställs över byggen och repon, CRACI:s inventering visar vilka versioner som ingick i vilka produkter, och varje jobbs nätverksspårning visar vart det anslöt, så att du kan se om ett jobb som installerade en infekterad version också nådde något ställe det inte borde ha nått.
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. CRACI poängsätter inte paket utifrån skadligt beteende; kombinera det med ett verktyg för paketrykte om du vill att infekterade versioner ska blockeras före installation.
Boka en demo för att se paketregistreringen för ett av dina egna byggen.
