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.

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 restante 77% era 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.

Quando viene annunciata una nuova ondata, ogni team si pone la stessa domanda: qualcuna delle nostre build ha installato una versione infetta? Questo articolo spiega che cosa fa il worm, perché la risposta abituale ("controlli il lockfile") è incompleta e come rispondere correttamente.

Le ondate finora

QuandoNomeChe 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.
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.
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.

Il payload è tornato a settembre 2026, in quattro pacchetti che hanno superato la nuova scansione antimalware di npm al momento della pubblicazione.

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.

Perché il lockfile non basta

Il consiglio standard è cercare le versioni interessate 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.

Come verificare se è stato colpito

  1. Si procuri l'elenco delle versioni interessate 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 interessate. 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.
  5. 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.
  6. Se una build ha installato una versione interessata, 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.

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.
  • 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.

Il ruolo di CRACI

CRACI esegue i Suoi job GitHub Actions sui propri runner (runs-on: craci), e un proxy sul runner che riconosce i pacchetti 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 interessate nelle build registrate da CRACI invece di ricostruirle da lockfile e log. Le vulnerabilità rilevate sono aggregate tra build e repository, l'inventario di CRACI mostra quali versioni sono entrate in quali prodotti e la traccia di rete di ogni job mostra a che cosa si è connesso, così può vedere se un job che ha installato una versione infetta ha anche raggiunto qualche destinazione che non doveva raggiungere.

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. CRACI non assegna punteggi ai pacchetti per comportamenti malevoli: lo abbini a uno strumento di reputazione dei pacchetti se vuole bloccare le versioni infette prima dell'installazione.

Prenoti una demo per vedere il registro dei pacchetti di una delle Sue build.