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.

Det meste af skaden sker i CI. I bølgen i november 2025 fandt Wiz, at kun 23 % af infektionerne var på udvikleres maskiner: De øvrige 77 % var 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.

Når en ny bølge bliver annonceret, stiller hvert team det samme spørgsmål: Installerede nogen af vores builds en inficeret version? Dette indlæg gennemgår, hvad ormen gør, hvorfor det sædvanlige svar ("tjek lockfilen") er ufuldstændigt, og hvordan du besvarer det ordentligt.

Bølgerne indtil nu

HvornårNavnHvad der var anderledes
September 2025Shai-HuludEt 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.
November 2025Sha1-Hulud: The Second ComingFlyttede 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"-testEn ændret version i en enkelt pakke, uden nævneværdig spredning.
Maj 2026TanStack84 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.
Maj 2026AntV639 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 2026Miasma32 @redhat-cloud-services-pakker, udgivet via injicerede workflows med gyldig provenance. Hver infektion bar en unikt krypteret payload, så hash-baseret detektion fejlede.
August 2026CHAINDROPPopulæ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.

Payloaden vendte tilbage igen i september 2026 i fire pakker, der slap forbi npm's nye malware-scanning ved udgivelse.

Hvad ormen gør i et CI-job

Detaljerne ændrer sig mellem bølgerne, men mønsteret holder:

  1. Den kører ved installation. Et preinstall- eller postinstall-script kører, så snart npm install eller npm ci når den inficerede version. Intet i din kode behøver at importere den.
  2. 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.
  3. 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.
  4. 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.
  5. Den opfører sig anderledes i CI. Payloaden fra november 2025 tjekkede efter GITHUB_ACTIONS, BUILDKITE, CIRCLE_SHA1 og lignende variabler, og i CI kørte den synkront og blev færdig med sit arbejde, før buildet fortsatte.

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-tool installerer en pakke, der ikke er en projektafhængighed, i npm-cachen. npm install -g i et opsætningstrin, et værktøj installeret i et build-script og en npm install i 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_modules eller npm-cache kan indeholde en version, den aktuelle lockfile ikke længere nævner.
  • npm ci installerer 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.

Sådan tjekker du, om du var berørt

  1. 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.
  2. Søg i hver lockfile på hver branch, der kørte CI i perioden, ikke kun default-branchen.
  3. Søg i selve buildene. Kig i CI-logs efter setup_bun.js, bun_environment.js eller 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.
  4. Tjek din GitHub-organisation for nye repositories opkaldt efter ormen, workflow-filer, du ikke har skrevet (for eksempel shai-hulud-workflow.yml, discussion.yaml eller formatter_*.yml), og selvhostede runnere, du ikke har registreret.
  5. Ryd cachene. Slet GitHub Actions-caches og npm-cachen på udvikleres maskiner og selvhostede runnere, så næste build installerer rene versioner.
  6. 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.

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.
  • 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 giv GITHUB_TOKEN kun 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.site eller 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.

Her passer CRACI ind

CRACI kører dine GitHub Actions-jobs på sine egne runnere (runs-on: craci), og en pakkebevidst proxy på runneren 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. Fund samles på tværs af builds og repositories, CRACI's oversigt viser, hvilke versioner der gik ind i hvilke produkter, og hvert jobs netværkstrace viser, hvad det oprettede forbindelse til, så du kan se, om et job, der installerede en inficeret version, også nåede et sted hen, det ikke burde.

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. CRACI vurderer ikke pakker for ondsindet adfærd, så kombinér det med et værktøj til pakkeomdømme, hvis du vil have inficerede versioner blokeret før installation.

Book en demo for at se pakkeregistreringen af et af dine egne builds.