Shai-Hulud er en selvspredende orm som infiserer npm-pakker, et angrep på programvareleverandørkjeden som verver hvert offer til å spre det videre. Den kjører når en pakke installeres, stjeler npm-tokens, GitHub-tokens og skynøkler fra maskinen og bruker npm-tokenet til å publisere infiserte versjoner av offerets andre pakker. Siden den første bølgen i september 2025 har den kommet tilbake gang på gang, og i mai 2026 publiserte opphavsmennene kildekoden, så hver bølge siden da kan komme fra en annen gruppe.
Mesteparten av skaden skjer i CI. I bølgen i november 2025 fant Wiz at bare 23 % av infeksjonene var på utviklermaskiner: De resterende 77 % var i CI/CD-pipelines, med GitHub Actions som den vanligste. Et bygg installerer hundrevis av pakker, har hemmelighetene som trengs for å publisere og rulle ut, og kjører hvert installasjonsskript uten at noen følger med.
Når en ny bølge blir kjent, stiller hvert team det samme spørsmålet: Installerte noen av byggene våre en infisert versjon? Dette innlegget tar for seg hva ormen gjør, hvorfor det vanlige svaret («sjekk lockfilen») er ufullstendig, og hvordan du svarer ordentlig.
Bølgene så langt
| Når | Navn | Hva som var annerledes |
|---|---|---|
| September 2025 | Shai-Hulud | Et postinstall-skript kjørte en bundle.js på 3,6 MB, brukte TruffleHog til å finne hemmeligheter og publiserte opptil 20 av vedlikeholderens pakker på nytt. Mer enn 500 pakker ble rammet, blant dem @ctrl/tinycolor og 18 @crowdstrike-pakker. |
| November 2025 | Sha1-Hulud: The Second Coming | Flyttet til preinstall, installerte Bun-kjøretiden for å kjøre en nyttelast på 10 MB, la bakdører i opptil 100 pakker per vedlikeholder og registrerte infiserte maskiner som selvhostede GitHub-runnere. Pakker fra Zapier, ENS, PostHog og Postman ble rammet, og Wiz talte mer enn 25 000 repositorier med lekkede hemmeligheter. |
| Desember 2025 | «3.0»-test | En modifisert versjon i én enkelt pakke, uten nevneverdig spredning. |
| Mai 2026 | TanStack | 84 ondsinnede versjoner av 42 @tanstack-pakker. Angriperen stjal aldri et npm-token: En pull_request_target-workflow, en forgiftet Actions-cache og et OIDC-token lest fra runnerens minne gjorde det mulig å publisere gjennom TanStacks egen trusted publishing. |
| Mai 2026 | AntV | 639 versjoner av 323 pakker. På Linux-runnere i GitHub Actions leste nyttelasten hemmeligheter fra minnet til runner-prosessen og omgikk dermed loggmaskeringen. GitHub ugyldiggjorde 61 274 npm-skrivetokens. |
| Juni 2026 | Miasma | 32 @redhat-cloud-services-pakker, publisert gjennom injiserte workflows med gyldig provenance. Hver infeksjon bar en unikt kryptert nyttelast, så hash-basert deteksjon sviktet. |
| August 2026 | CHAINDROP | Populære cachepakker som keyv, cacheable og cache-manager, med hundrevis av berørte pakker totalt. Den ble værende gjennom konfigurasjonsfiler for editorer og AI-assistenter i repositoriet. |
Nyttelasten kom tilbake igjen i september 2026, i fire pakker som slapp gjennom npms nye skanning etter skadevare ved publisering.
Dette gjør ormen i en CI-jobb
Detaljene endrer seg mellom bølgene, men mønsteret består:
- Den kjører ved installasjon. Et
preinstall- ellerpostinstall-skript kjører så snartnpm installellernpm cinår den infiserte versjonen. Ingenting i koden din trenger å importere den. - Den samler hemmeligheter. Miljøvariabler,
.npmrc-tokens, skylegitimasjon fra metadatatjenesten og hemmelighetshåndterere, og alt TruffleHog finner på disk. Senere bølger leste minnet til GitHub Actions-runneren direkte, noe som gir hemmeligheter som aldri ble eksponert for steget, og som maskeringen ville ha skjult i loggene. - Den får dem ut. Tidlige bølger opprettet offentlige GitHub-repositorier oppkalt etter ormen og pushet en workflow som sendte hver repositoriehemmelighet til
webhook.site. Senere bølger brukte angripernes egne servere og meldingsnettverk. - Den sprer seg og blir værende. Med et stjålet npm-token publiserer den infiserte versjoner av andre pakker. Med et GitHub-token legger den til workflows, registrerer selvhostede runnere eller planter konfigurasjonsfiler som kjører neste gang noen åpner repositoriet.
- Den oppfører seg annerledes i CI. Nyttelasten fra november 2025 sjekket etter
GITHUB_ACTIONS,BUILDKITE,CIRCLE_SHA1og lignende variabler, og i CI kjørte den synkront og gjorde seg ferdig før bygget gikk videre.
Derfor avgjør ikke lockfilen saken
Standardrådet er å søke etter de berørte versjonene i package-lock.json, yarn.lock eller pnpm-lock.yaml. Gjør det først, men en ren lockfile beviser ikke et rent bygg:
- Pakker installert utenfor lockfilen.
npx some-toolinstallerer en pakke som ikke er en prosjektavhengighet, i npm-cachen.npm install -gi et oppsettsteg, et verktøy installert inne i et byggeskript og ennpm installi en Dockerfile går alle utenom prosjektets lockfile. - Lockfilen på grenen i dag er ikke lockfilen som kjørte. Et bygg for tre uker siden brukte det lockfilen sa da, på den grenen som utløste det. Pull requests med avhengighetsoppdateringer kjører CI mot nye versjoner før noen har gått gjennom dem.
- Cacher gjenoppretter det som ble installert før. En gjenopprettet
node_moduleseller npm-cache kan inneholde en versjon dagens lockfile ikke lenger nevner. npm ciinstallerer det lockfilen sier. Hvis den infiserte versjonen allerede var låst, installerer en streng installasjon den trofast.- En korrekt publisert versjon kan likevel være ondsinnet. TanStack og Miasma ble kompromittert gjennom sine egne utgivelsesworkflows, og Miasmas infiserte versjoner hadde gyldig provenance. Å sjekke at en versjon ble riktig publisert, forteller deg ikke at den er ren.
Det du faktisk trenger, er en registrering av hvilke pakkeversjoner hvert bygg hentet, på tvers av hvert repositorium og hver gren, for hele perioden en infisert versjon lå i registeret.
Slik sjekker du om du ble rammet
- Skaff listen over berørte versjoner for bølgen. Sikkerhetsleverandører publiserer dem i løpet av timer. Datadog har for eksempel et repositorium med kompromitteringsindikatorer med pakke- og versjonslister.
- Søk i hver lockfile, på hver gren som kjørte CI i perioden, ikke bare standardgrenen.
- Søk i selve byggene. Se i CI-logger etter
setup_bun.js,bun_environment.jseller andre filnavn som nevnes i varselet, og etter installasjonsutdata som nevner de berørte versjonene. Hvis du har SBOM-er per bygg, søker du etter versjonene i dem. - Sjekk GitHub-organisasjonen din for nye repositorier oppkalt etter ormen, workflow-filer du ikke har skrevet (for eksempel
shai-hulud-workflow.yml,discussion.yamlellerformatter_*.yml) og selvhostede runnere du ikke har registrert. - Tøm cachene. Slett GitHub Actions-cacher og npm-cachen på utviklermaskiner og selvhostede runnere, så neste bygg installerer rene versjoner.
- Hvis et bygg installerte en berørt versjon, behandler du jobben som kompromittert. Roter hver hemmelighet den kunne lese: npm-tokens, GitHub-tokens, skylegitimasjon og deploy-nøkler. Siden senere bølger publiserte stjålne hemmeligheter i andre ofres kontoer, bør du ikke anta at du er trygg fordi ingenting dukket opp i dine egne.
Slik begrenser du skaden neste gang
- Slå av installasjonsskript der du kan. npm v12, utgitt i juli 2026, deaktiverer installasjonsskript som standard og lar deg tillate dem per pakke. pnpm og Yarn har lignende kontroller.
- Vent før du tar i bruk nye versjoner. De fleste infiserte versjoner blir funnet og fjernet i løpet av timer eller dager. En ventetid på avhengighetsoppdateringer, som både Dependabot og Renovate støtter, holder dem ute av byggene dine.
- Publiser med trusted publishing og kortlivede tokens. npm tilbakekalte alle classic tokens i desember 2025. Bruk OIDC-basert trusted publishing fra CI i stedet for langlivede tokens, og hold publiseringsworkflows atskilt fra bygge- og testworkflows.
- Herd workflowene ormen misbruker. Unngå
pull_request_target, fest actions til commit-SHA-er, og giGITHUB_TOKENbare tillatelsene hver jobb trenger. Se GitHub Actions-hemmeligheter: slik lekker de og slik låser du dem ned. - Begrens hvor bygg kan koble seg til. Et bygg trenger pakkeregistrene sine og noen få API-er. Kan det bare nå disse, feiler ormens kall til
webhook.siteeller dens egne servere. Se slik stopper en egress-policy eksfiltrering fra GitHub Actions. Dette stopper ikke eksfiltrering gjennom en vert bygget bruker legitimt, for eksempel GitHubs eget API, som noen bølger utnyttet.
Der CRACI kommer inn
CRACI kjører GitHub Actions-jobbene dine på egne runnere (runs-on: craci), og en pakkebevisst proxy på runneren registrerer hver pakke hver jobb faktisk henter fra npm og andre registre, inkludert pakker gjenopprettet fra cacher. Denne registreringen dekker blindsonene over: npx-verktøy, globale installasjoner og installasjoner inne i byggeskript dukker alle opp, fordi registreringen kommer fra det jobben lastet ned, ikke fra det en lockfile oppga.
Når en ny bølge blir kjent, slår du opp de berørte versjonene i byggene CRACI har registrert, i stedet for å rekonstruere dem fra lockfiler og logger. Funn samles på tvers av bygg og repositorier, CRACIs inventar viser hvilke versjoner som gikk inn i hvilke produkter, og hver jobbs nettverksspor viser hva den koblet seg til, så du kan se om en jobb som installerte en infisert versjon, også nådde et sted den ikke skulle.
CRACIs egress-policy begrenser hvor en jobb kan koble seg til, valideres før jobben starter og blokkerer ved feil, og varsler deg når en jobb prøver å nå en destinasjon policyen ikke tillater. CRACI gir ikke pakker en poengsum for ondsinnet atferd. Kombiner det med et verktøy for pakkeomdømme hvis du vil ha infiserte versjoner blokkert før installasjon.
Book en demo for å se pakkeregistreringen fra et av dine egne bygg.
