Shai-Hulud è un worm autopropagante che infetta i pacchetti npm, un attacco alla software supply chain che arruola ogni vittima per diffondersi ulteriormente. Entra in azione quando un pacchetto viene installato, ruba token npm, token GitHub e chiavi cloud dalla macchina e usa il token npm per pubblicare versioni infette degli altri pacchetti della vittima. Dalla prima ondata di settembre 2025 è tornato più volte, e a maggio 2026 i suoi autori ne hanno pubblicato il codice sorgente, quindi ogni ondata successiva può provenire da un gruppo diverso. Se una delle Sue build ha installato una versione infetta, consideri quel job compromesso e ruoti ogni secret che poteva leggere.
La maggior parte dei danni avviene in CI. Nell'ondata di novembre 2025, Wiz ha rilevato che solo il 23% delle infezioni si trovava su macchine di sviluppatori, il resto per lo più in pipeline CI/CD, con GitHub Actions in testa. Una build installa centinaia di pacchetti, contiene i secret necessari per pubblicare e distribuire, ed esegue ogni script di installazione senza che nessuno guardi. Così, quando viene annunciata una nuova ondata, ogni team si pone la stessa domanda: qualcuna delle nostre build ha installato una versione infetta?
Che cosa è successo
| Quando | Ondata | Che cosa è cambiato |
|---|---|---|
| Settembre 2025 | Shai-Hulud | Uno script postinstall eseguiva un bundle.js da 3,6 MB, usava TruffleHog per trovare i secret e ripubblicava fino a 20 pacchetti del maintainer. Sono stati colpiti più di 500 pacchetti, tra cui @ctrl/tinycolor e 18 pacchetti @crowdstrike. La CISA ha pubblicato un alert il 23 settembre. |
| Novembre 2025 | Sha1-Hulud: The Second Coming | Passato a preinstall, installava il runtime Bun per eseguire un payload da 10 MB, inseriva backdoor in un massimo di 100 pacchetti per maintainer e registrava le macchine infette come runner GitHub self-hosted. Sono stati colpiti pacchetti di Zapier, ENS, PostHog e Postman; Wiz ha contato più di 25.000 repository con secret trapelati. |
| Dicembre 2025 | Test "3.0" | Una versione modificata in un singolo pacchetto, senza una diffusione significativa. |
| Maggio 2026 | TanStack | 84 versioni malevole di 42 pacchetti @tanstack. L'attaccante non ha mai rubato un token npm: un workflow pull_request_target, una cache di Actions avvelenata e un token OIDC letto dalla memoria del runner gli hanno permesso di pubblicare tramite il trusted publishing di TanStack. Tracciata come CVE-2026-45321, aggiunta al catalogo KEV della CISA il 27 maggio. |
| Maggio 2026 | AntV | 639 versioni di 323 pacchetti. Sui runner Linux di GitHub Actions il payload leggeva i secret dalla memoria del processo del runner, aggirando il mascheramento nei log. GitHub ha invalidato 61.274 token di scrittura npm. |
| Giugno 2026 | Miasma | 32 pacchetti @redhat-cloud-services, pubblicati tramite workflow iniettati con provenance valida. Ogni infezione conteneva un payload cifrato in modo univoco, quindi il rilevamento basato su hash non ha funzionato. |
| Agosto 2026 | CHAINDROP | Popolari pacchetti di caching come keyv, cacheable e cache-manager, con centinaia di pacchetti colpiti in totale. Persisteva tramite file di configurazione di editor e assistenti IA nel repository. |
| Settembre 2026 | Ricomparsa | Il 7 settembre il payload è tornato in quattro pacchetti che hanno superato la nuova scansione antimalware di npm al momento della pubblicazione. |
| Ottobre 2026 | tensorlake | L'8 ottobre è stato pubblicato [email protected] con il worm. OX Security ha osservato nuove chiavi dell'attaccante e ipotizza "un attore di minaccia indipendente o un nuovo gruppo che usa il nome Shai-Hulud". |
Che cosa fa il worm in un job CI
I dettagli cambiano da un'ondata all'altra, ma lo schema resta:
- Entra in azione all'installazione. Uno script
preinstallopostinstallviene eseguito non appenanpm installonpm ciraggiunge la versione infetta. Nulla nel Suo codice deve importarlo. - Raccoglie i secret. Variabili d'ambiente, token in
.npmrc, credenziali cloud dal servizio di metadati e dai secret manager, e tutto ciò che TruffleHog trova su disco. Le ondate successive hanno letto direttamente la memoria del runner di GitHub Actions, ottenendo secret mai esposti allo step e che il mascheramento avrebbe nascosto nei log. - Li porta fuori. Le prime ondate creavano repository GitHub pubblici con il nome del worm e inserivano un workflow che inviava ogni secret del repository a
webhook.site. Le ondate successive hanno usato server degli attaccanti e reti di messaggistica. - Si diffonde e resta. Con un token npm rubato pubblica versioni infette di altri pacchetti. Con un token GitHub aggiunge workflow, registra runner self-hosted o inserisce file di configurazione che vengono eseguiti alla successiva apertura del repository.
- Si comporta diversamente in CI. Il payload di novembre 2025 controllava
GITHUB_ACTIONS,BUILDKITE,CIRCLE_SHA1e variabili simili, e in CI veniva eseguito in modo sincrono, terminando il suo lavoro prima che la build proseguisse.
Chi è colpito
Chiunque abbia macchine o build che hanno installato una versione infetta mentre era sul registry: laptop degli sviluppatori, job CI, build Docker che eseguono npm install e runner self-hosted. Non serve dipendere direttamente dal pacchetto; basta una dipendenza transitiva o uno strumento eseguito con npx. Ogni ondata ha il proprio elenco di pacchetti e versioni, e le versioni infette vengono di solito rimosse nel giro di ore o giorni, quindi ciò che conta è se qualcosa le ha installate in quella finestra.
Come funziona
Shai-Hulud non è un bug di npm. È codice malevolo pubblicato all'interno di pacchetti per il resto legittimi, che gli advisory di malware di GitHub per i pacchetti infetti classificano come codice malevolo incorporato (CWE-506). Ogni ondata combina alcune tecniche:
- Esecuzione all'installazione. npm esegue gli script del ciclo di vita di un pacchetto, come
preinstall,postinstalleprepare, durante l'installazione, con i permessi dell'utente o del job CI. Basta che una versione infetta sia nell'albero delle dipendenze; nessuno deve importarla. - Propagazione con diritti di pubblicazione rubati. Con un token npm o GitHub sottratto a una vittima, il worm pubblica nuove versioni degli altri pacchetti di quella vittima con il payload aggiunto, così ogni maintainer infettato infetta altri pacchetti.
- Pubblicazione tramite la pipeline di rilascio della vittima stessa (TanStack, maggio 2026). Secondo l'advisory di GitHub, l'attaccante ha concatenato un workflow
pull_request_target, una cache di GitHub Actions avvelenata attraverso il confine di fiducia tra fork e repository base, e un token OIDC letto dalla memoria del runner, e ha pubblicato tramite il trusted publishing di TanStack senza modificare il workflow di pubblicazione. Le versioni infette dichiaravano una dipendenza opzionale da un commit in un fork, il cui scriptprepareeseguiva il payload e poi falliva di proposito, così npm la scartava senza lasciare traccia innode_modules. - Provenance valida (Miasma, giugno 2026). Workflow iniettati hanno pubblicato dai repository delle vittime stesse, quindi le versioni infette avevano una provenance valida, e un payload cifrato in modo unico per ogni infezione eludeva il rilevamento basato su hash.
Come verificare se è colpito
- Si procuri l'elenco delle versioni colpite per l'ondata. I vendor di sicurezza le pubblicano nel giro di poche ore; Datadog, per esempio, mantiene un repository di indicatori di compromissione con elenchi di pacchetti e versioni.
- Cerchi in ogni lockfile, su ogni branch che ha eseguito la CI durante la finestra, non solo sul branch predefinito.
- Cerchi nelle build stesse. Cerchi nei log CI
setup_bun.js,bun_environment.jso altri nomi di file citati nell'advisory, e output di installazione che menzionino le versioni colpite. Se conserva le SBOM per build, cerchi in esse le versioni. - Controlli la Sua organizzazione GitHub per individuare nuovi repository con il nome del worm, file di workflow che non ha scritto (per esempio
shai-hulud-workflow.yml,discussion.yamloformatter_*.yml) e runner self-hosted che non ha registrato.
Comandi che può adattare. Per cercare nei lockfile di ogni branch remoto, inserisca le versioni colpite dell'ondata in affected.txt, una name@version per riga:
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
Per elencare i runner self-hosted della Sua organizzazione, i nuovi repository che menzionano il worm e i file di workflow che il worm notoriamente crea (sostituisca 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
Anche i singoli repository possono avere runner propri (gh api repos/ORG/REPO/actions/runners), e le prime ondate creavano repository negli account personali degli sviluppatori di cui avevano rubato i token, quindi chieda ai Suoi sviluppatori di controllare i propri.
Perché il lockfile non basta
Il consiglio standard è cercare le versioni colpite in package-lock.json, yarn.lock o pnpm-lock.yaml. Lo faccia per prima cosa, ma un lockfile pulito non dimostra che la build sia pulita:
- Pacchetti installati fuori dal lockfile.
npx some-toolinstalla nella cache npm un pacchetto che non è una dipendenza del progetto.npm install -gin uno step di setup, uno strumento installato dentro uno script di build e unnpm installin un Dockerfile aggirano tutti il lockfile del progetto. - Il lockfile di oggi sul branch non è quello che è stato eseguito. Una build di tre settimane fa ha usato ciò che il lockfile diceva allora, su qualunque branch l'abbia attivata. Le pull request di aggiornamento delle dipendenze eseguono la CI sulle nuove versioni prima che qualcuno le esamini.
- Le cache ripristinano ciò che era stato installato prima. Una cartella
node_moduleso una cache npm ripristinata può contenere una versione che il lockfile attuale non menziona più. npm ciinstalla ciò che dice il lockfile. Se la versione infetta era già bloccata, un'installazione rigorosa la installa fedelmente.- Una versione pubblicata correttamente può comunque essere malevola. TanStack e Miasma sono stati compromessi tramite i propri workflow di release, e le versioni infette di Miasma avevano una provenance valida. Verificare che una versione sia stata pubblicata correttamente non dice che sia pulita.
Ciò che serve davvero è un registro delle versioni dei pacchetti scaricate da ogni build, in tutti i repository e tutti i branch, per l'intero periodo in cui una versione infetta è stata sul registry. Veda siamo colpiti? come valutare una nuova CVE in tutte le build.
Come correggere e che cosa fare se non può ancora applicare la patch
- Se una build ha installato una versione colpita, consideri il job compromesso. Ruoti ogni secret che poteva leggere: token npm, token GitHub, credenziali cloud e chiavi di deploy. Poiché le ondate successive hanno pubblicato i secret rubati negli account di altre vittime, non si consideri al sicuro solo perché nel Suo non è apparso nulla.
- Svuoti le cache. Elimini le cache di GitHub Actions e la cache npm sulle macchine degli sviluppatori e sui runner self-hosted, così la build successiva installa versioni pulite:
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
- Fissi una versione pulita e rimuova i workflow, i repository, i runner e i file di configurazione aggiunti dal worm.
Verificare la pulizia
- Ogni lockfile si risolve in versioni pulite. Su ogni branch,
npm ls <package>(opnpm whyoyarn why) mostra la versione installata; nessuna deve comparire nell'elenco dell'ondata. - Un'installazione pulita non contiene file del payload. Dopo lo svuotamento delle cache descritto sopra, installi di nuovo e cerchi i file del payload indicati negli advisory:
find node_modules -name bundle.js -size +3M \
-o -name setup_bun.js -o -name bun_environment.js -o -name router_init.js
- Le vecchie credenziali non funzionano più. I token npm revocati non compaiono più in
npm token list, i vecchi token GitHub e le chiavi di deploy sono eliminati e le chiavi cloud sono disattivate, non solo sostituite. - Le cache sono sparite.
gh cache listnon mostra alcuna cache creata prima della pulizia in nessun repository colpito. - Non resta nulla di ciò che ha aggiunto il worm: nessun repository, file di workflow, runner self-hosted o file di configurazione di editor e assistenti AI che non abbia creato Lei.
- La build successiva è pulita. Controlli le versioni che ha effettivamente installato, dal suo log o dalla SBOM per build, non solo dal lockfile.
Come ridurre i danni la prossima volta
- Disattivi gli script di installazione dove possibile. npm v12, rilasciato a luglio 2026, disattiva gli script di installazione per impostazione predefinita e consente di abilitarli per pacchetto. pnpm e Yarn offrono controlli simili.
- Attenda prima di adottare nuove versioni. La maggior parte delle versioni infette viene individuata e rimossa nel giro di ore o giorni. Un periodo di cooldown sugli aggiornamenti delle dipendenze, supportato sia da Dependabot sia da Renovate, le tiene fuori dalle Sue build. Il package firewall di GitHub, in public preview per GitHub Enterprise Cloud dal 20 ottobre 2026, controlla le installazioni npm alla ricerca di pacchetti malevoli e offre anch'esso i cooldown.
- Pubblichi con trusted publishing e token di breve durata. npm ha revocato tutti i token classici a dicembre 2025. Usi il trusted publishing basato su OIDC dalla CI invece di token di lunga durata, e mantenga i workflow di pubblicazione separati da quelli di build e test.
- Rafforzi i workflow di cui il worm abusa. Eviti
pull_request_target, fissi le action agli SHA di commit e dia aGITHUB_TOKENsolo i permessi di cui ogni job ha bisogno. Veda Secret di GitHub Actions: come trapelano e come proteggerli. - Limiti dove le build possono connettersi. Una build ha bisogno dei suoi registry di pacchetti e di alcune API. Se può raggiungere solo quelli, le chiamate del worm a
webhook.siteo ai propri server falliscono. Veda come una policy di egress ferma l'esfiltrazione da GitHub Actions. Questo non ferma l'esfiltrazione attraverso un host che la build usa legittimamente, come l'API di GitHub stessa, su cui si sono basate alcune ondate.
Verificare una possibile compromissione
Shai-Hulud è malevolo per progettazione: qualsiasi macchina o job che ha installato una versione infetta lo ha eseguito. Gli indicatori variano a seconda dell'ondata, quindi usi l'advisory dell'ondata che sta verificando. Dalle fonti primarie:
| Ondata | Indicatori | Fonte |
|---|---|---|
| Settembre 2025 | Payload bundle.js, SHA-256 46faab8ab153fae6e80e7cca38eab363075bb524edd79e42269217a083628f09; workflow .github/workflows/shai-hulud-workflow.yml su un branch shai-hulud; un repository pubblico chiamato Shai-Hulud; connessioni a webhook.site | StepSecurity, CISA |
| Novembre 2025 | setup_bun.js, SHA-1 d1829b4708126dcc7bea7437c04d1f10eacd4a16; bun_environment.js, SHA-1 d60ec97eea19fffb4809bc35b91033b52490ca11 o 3d7570d14d34b0ba137d502f042b27b0f37a59fa; repository con cloud.json, contents.json, environment.json, truffleSecrets.json e actionsSecrets.json; workflow .github/workflows/discussion.yaml e formatter_*.yml; un runner self-hosted chiamato SHA1HULUD | Wiz |
| Maggio 2026, TanStack | "@tanstack/setup": "github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c" in optionalDependencies; payload router_init.js (circa 2,3 MB) nella radice del pacchetto; helper tanstack_runner.js; connessioni a filev2.getsession.org e da seed1.getsession.org a seed3.getsession.org, e download di secondo stadio da litter.catbox.moe | GHSA-g7cv-rxg3-hmpx |
Le ondate successive cambiavano il payload a ogni infezione, quindi un hash di file che non corrisponde non prova nulla; confronti prima le versioni dei pacchetti.
Quando presumere la compromissione. Consideri compromessi tutti gli host e i job CI che hanno installato una versione colpita, anche se l'installazione è fallita o non ha lasciato nulla in node_modules. L'advisory di GitHub su TanStack afferma che qualsiasi ambiente di sviluppo o CI che ha installato una versione colpita "deve essere considerato compromesso". Verifichi a partire dalla prima pubblicazione della versione infetta, non dal giorno in cui l'ondata è stata annunciata.
Ruoti le credenziali. Ruoti tutto ciò che l'installazione poteva raggiungere. Nell'ondata TanStack questo ha significato credenziali AWS, GCP, Kubernetes, Vault, GitHub, npm e SSH, secondo il postmortem. Esamini i log di audit del cloud alla ricerca di attività provenienti dagli host colpiti durante e dopo la finestra di installazione.
Elimini cache e artefatti. Cerchi nei repository di artefatti e nei proxy delle dipendenze le copie in cache delle versioni colpite e le elimini, come consiglia la CISA. Elimini node_modules, la cache npm e le cache CI; nell'ondata TanStack il punto d'ingresso dell'attaccante è stata una cache dello store pnpm avvelenata, quindi controlli le cache di Actions oltre alle cache dei pacchetti. Ricostruisca e ripubblichi tutto ciò che un job compromesso ha prodotto.
Che cosa dire ai Suoi clienti
Se distribuisce software costruito con npm, i clienti chiederanno se il Suo prodotto contiene il worm o se la Sua pipeline di release è stata compromessa. Risponda con ciò che ha verificato e ciò che ha fatto:
Abbiamo confrontato ogni build di [prodotto] tra il [data] e il [data] con gli elenchi pubblicati delle versioni di pacchetti npm infettate da Shai-Hulud. [Nessuna build ha installato una versione colpita.] / [Le build di [componente] del [data] hanno installato [pacchetto@versione]; abbiamo ruotato le credenziali a cui quelle build potevano accedere, ricostruito [prodotto] da dipendenze pulite e rilasciato la versione [versione]. Le release [x–y] sono colpite; vi invitiamo ad aggiornare alla versione [versione].] Aggiorneremo questo avviso se nuove ondate cambieranno il quadro.
Se sta ancora verificando, lo dica, e dica quando risponderà. Per una prima dichiarazione e modelli per ogni esito, consulti come rispondere ai clienti dopo un attacco alla supply chain.
Il ruolo di CRACI
CRACI esegue i Suoi job GitHub Actions sui propri runner (runs-on: craci) e registra ogni pacchetto che ciascun job scarica effettivamente da npm e da altri registry, compresi i pacchetti ripristinati dalle cache. Quel registro copre i punti ciechi descritti sopra: strumenti npx, installazioni globali e installazioni dentro gli script di build compaiono tutti, perché il registro nasce da ciò che il job ha scaricato, non da ciò che dichiarava un lockfile. Quando viene annunciata una nuova ondata, cerca le versioni colpite nelle build registrate da CRACI invece di ricostruirle da lockfile e log, e la traccia di rete di ogni job mostra a che cosa si è connesso. La cronologia delle build conserva ogni build passata con la sua SBOM, così vede quali build contenevano una versione infetta e la finestra in cui è stato esposto, entro il Suo periodo di conservazione.
La policy di egress di CRACI limita dove un job può connettersi, viene convalidata prima dell'avvio del job, opera in modalità fail closed e La avvisa quando un job tenta di raggiungere una destinazione non consentita dalla policy. Le release infette con una CVE pubblicata, come l'ondata TanStack, vengono segnalate come qualsiasi altra vulnerabilità, e i policy gate possono bloccare le build che le contengono. CRACI non assegna punteggi ai pacchetti per comportamenti malevoli e non segnala una nuova versione infetta prima che esista un advisory: lo abbini a uno strumento di reputazione dei pacchetti se vuole bloccare le versioni infette prima dell'installazione.
Veda il registro dei pacchetti di una delle Sue build
Altri incidenti: tutte le vulnerabilità.
Fonti e aggiornamenti
Fonti primarie e ricerche originali:
- CISA: alert del 23 settembre 2025, catalogo KEV
- GitHub: GHSA-g7cv-rxg3-hmpx (CVE-2026-45321), il piano di GitHub per una supply chain npm più sicura, revoca dei token classici npm
- Postmortem di TanStack
- Wiz sull'ondata di novembre 2025 e sulle sue conseguenze
- Microsoft su AntV e le linee guida di rilevamento di Microsoft
- Datadog ed Elastic su CHAINDROP
- StepSecurity sull'ondata di settembre 2025
- FIRST: punteggio EPSS per CVE-2026-45321
Aggiornamenti:
- 2026-11-11: aggiunti al riquadro l'EPSS, la classe CWE e il credito, controlli pronti da copiare e sezioni su come funziona il worm, sulla verifica della pulizia e sul controllo di una possibile compromissione, con indicatori per ondata.
- 2026-11-11: spostato dal blog alla sezione vulnerabilità. Aggiunte l'ondata tensorlake dell'8 ottobre 2026, la data della ricomparsa di settembre 2026, la CVE-2026-45321 con il suo inserimento nel KEV per l'ondata TanStack e il package firewall di GitHub.
- 2026-09-25: prima pubblicazione sul blog di CRACI.