131% di CVE segnalate in più al giorno nel 2026 rispetto all'anno scorso.
← Tutte le vulnerabilità

Shai-Hulud: il worm npm e come verificare se le Sue build sono colpite

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

QuandoOndataChe cosa è cambiato
Settembre 2025Shai-HuludUno 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 2025Sha1-Hulud: The Second ComingPassato 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 2025Test "3.0"Una versione modificata in un singolo pacchetto, senza una diffusione significativa.
Maggio 2026TanStack84 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 2026AntV639 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 2026Miasma32 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 2026CHAINDROPPopolari 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 2026RicomparsaIl 7 settembre il payload è tornato in quattro pacchetti che hanno superato la nuova scansione antimalware di npm al momento della pubblicazione.
Ottobre 2026tensorlakeL'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:

  1. Entra in azione all'installazione. Uno script preinstall o postinstall viene eseguito non appena npm install o npm ci raggiunge la versione infetta. Nulla nel Suo codice deve importarlo.
  2. 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.
  3. 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.
  4. 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.
  5. Si comporta diversamente in CI. Il payload di novembre 2025 controllava GITHUB_ACTIONS, BUILDKITE, CIRCLE_SHA1 e 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, postinstall e prepare, 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 script prepare eseguiva il payload e poi falliva di proposito, così npm la scartava senza lasciare traccia in node_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

  1. 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.
  2. Cerchi in ogni lockfile, su ogni branch che ha eseguito la CI durante la finestra, non solo sul branch predefinito.
  3. Cerchi nelle build stesse. Cerchi nei log CI setup_bun.js, bun_environment.js o 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.
  4. 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.yaml o formatter_*.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-tool installa nella cache npm un pacchetto che non è una dipendenza del progetto. npm install -g in uno step di setup, uno strumento installato dentro uno script di build e un npm install in 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_modules o una cache npm ripristinata può contenere una versione che il lockfile attuale non menziona più.
  • npm ci installa 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

  1. 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.
  2. 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
  1. Fissi una versione pulita e rimuova i workflow, i repository, i runner e i file di configurazione aggiunti dal worm.

Verificare la pulizia

  1. Ogni lockfile si risolve in versioni pulite. Su ogni branch, npm ls <package> (o pnpm why o yarn why) mostra la versione installata; nessuna deve comparire nell'elenco dell'ondata.
  2. 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
  1. 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.
  2. Le cache sono sparite. gh cache list non mostra alcuna cache creata prima della pulizia in nessun repository colpito.
  3. 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.
  4. 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 a GITHUB_TOKEN solo 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.site o 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:

OndataIndicatoriFonte
Settembre 2025Payload 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.siteStepSecurity, CISA
Novembre 2025setup_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 SHA1HULUDWiz
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.moeGHSA-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:

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.

Domande frequenti

Shai-Hulud è ancora attivo?

Sì. Torna a ondate da settembre 2025 e i suoi autori ne hanno pubblicato il codice sorgente a maggio 2026, quindi le ondate successive possono provenire da gruppi diversi. L'ondata più recente trattata qui ha colpito il pacchetto tensorlake l'8 ottobre 2026.

Shai-Hulud ha una CVE?

Il worm nel suo complesso no. GitHub pubblica advisory sul malware per i singoli pacchetti infetti, e l'ondata TanStack di maggio 2026 ha la CVE-2026-45321, che la CISA ha aggiunto al suo catalogo Known Exploited Vulnerabilities il 27 maggio 2026.

Un lockfile pulito significa che non siamo stati colpiti?

No. Pacchetti installati con npx, installazioni globali, installazioni dentro script di build e Dockerfile, lockfile più vecchi su altri branch e cache ripristinate possono tutti introdurre una versione infetta che il lockfile attuale non mostra. Serve un registro di ciò che ogni build ha effettivamente scaricato.

Come verifico se sono stato colpito?

Si procuri le versioni dei pacchetti colpite dall'ondata, cerchi in ogni lockfile su ogni branch che ha eseguito la CI nella finestra, cerchi nei log CI e nelle SBOM per build le versioni e i nomi dei file del payload, e controlli la Sua organizzazione GitHub alla ricerca di repository, workflow e runner self-hosted che non ha creato.

Che cosa faccio se una build ha installato una versione infetta?

Consideri il job compromesso. Ruoti ogni secret che poteva leggere, compresi token npm, token GitHub, credenziali cloud e chiavi di deploy, elimini le cache di GitHub Actions e le cache npm e controlli se il worm ha aggiunto workflow, repository e runner.

Come faccio a sapere se il worm è stato eseguito nelle nostre build?

Cerchi nei log di build, nei lockfile e nei pacchetti installati gli indicatori dell'ondata: file del payload come bundle.js, setup_bun.js, bun_environment.js o router_init.js, repository e workflow che non ha creato Lei, un runner self-hosted chiamato SHA1HULUD e connessioni a webhook.site o getsession.org. Ogni job che ha installato una versione colpita ha eseguito il payload, quindi lo consideri compromesso anche se non trova nient'altro.