O Shai-Hulud é um worm autorreplicante que infeta pacotes npm, um ataque à cadeia de abastecimento de software que recruta cada vítima para o propagar. É executado quando um pacote é instalado, rouba tokens npm, tokens GitHub e chaves cloud da máquina e usa o token npm para publicar versões infetadas dos outros pacotes da vítima. Desde a primeira vaga, em setembro de 2025, tem regressado repetidamente e, em maio de 2026, os seus autores publicaram o código-fonte, pelo que cada vaga desde então pode ser de um grupo diferente.
A maior parte dos danos acontece em CI. Na vaga de novembro de 2025, a Wiz concluiu que apenas 23% das infeções ocorreram em máquinas de programadores: os outros 77% ocorreram em pipelines de CI/CD, sendo o GitHub Actions o mais comum. Um build instala centenas de pacotes, contém os segredos necessários para publicar e implementar e executa todos os scripts de instalação sem ninguém a vigiar.
Quando é anunciada uma nova vaga, todas as equipas fazem a mesma pergunta: algum dos nossos builds instalou uma versão infetada? Este artigo explica o que o worm faz, porque é que a resposta habitual ("verificar o lockfile") é incompleta e como responder corretamente.
As vagas até agora
| Quando | Nome | O que foi diferente |
|---|---|---|
| Setembro de 2025 | Shai-Hulud | Um script postinstall executava um bundle.js de 3,6 MB, usava o TruffleHog para encontrar segredos e republicava até 20 pacotes do mantenedor. Foram afetados mais de 500 pacotes, incluindo @ctrl/tinycolor e 18 pacotes @crowdstrike. |
| Novembro de 2025 | Sha1-Hulud: The Second Coming | Passou para preinstall, instalou o runtime Bun para executar um payload de 10 MB, colocou backdoors em até 100 pacotes por mantenedor e registou máquinas infetadas como runners self-hosted do GitHub. Foram atingidos pacotes da Zapier, ENS, PostHog e Postman; a Wiz contou mais de 25 000 repositórios com segredos expostos. |
| Dezembro de 2025 | Teste "3.0" | Uma versão modificada num único pacote, sem propagação relevante. |
| Maio de 2026 | TanStack | 84 versões maliciosas de 42 pacotes @tanstack. O atacante nunca roubou um token npm: um workflow pull_request_target, uma cache do Actions envenenada e um token OIDC lido da memória do runner permitiram-lhe publicar através do próprio trusted publishing da TanStack. |
| Maio de 2026 | AntV | 639 versões de 323 pacotes. Nos runners Linux do GitHub Actions, o payload lia segredos da memória do processo do runner, contornando a ocultação nos registos. O GitHub invalidou 61 274 tokens de escrita do npm. |
| Junho de 2026 | Miasma | 32 pacotes @redhat-cloud-services, publicados através de workflows injetados com proveniência válida. Cada infeção levava um payload cifrado de forma única, pelo que a deteção baseada em hashes falhou. |
| Agosto de 2026 | CHAINDROP | Pacotes populares de cache, como keyv, cacheable e cache-manager, com centenas de pacotes afetados no total. Persistia através de ficheiros de configuração de editores e assistentes de IA no repositório. |
O payload voltou em setembro de 2026, em quatro pacotes que passaram pela nova análise de malware do npm no momento da publicação.
O que o worm faz num job de CI
Os detalhes mudam de vaga para vaga, mas o padrão mantém-se:
- É executado na instalação. Um script
preinstalloupostinstallé executado assim quenpm installounpm cichega à versão infetada. Nada no seu código tem de o importar. - Recolhe segredos. Variáveis de ambiente, tokens de
.npmrc, credenciais cloud do serviço de metadados e de gestores de segredos, e tudo o que o TruffleHog encontre em disco. As vagas posteriores liam diretamente a memória do runner do GitHub Actions, o que dá acesso a segredos que nunca foram expostos ao passo e que a ocultação teria escondido nos registos. - Envia-os para fora. As primeiras vagas criavam repositórios GitHub públicos com o nome do worm e enviavam um workflow que mandava todos os segredos do repositório para
webhook.site. As vagas posteriores usavam servidores do atacante e redes de mensagens. - Propaga-se e permanece. Com um token npm roubado, publica versões infetadas de outros pacotes. Com um token GitHub, adiciona workflows, regista runners self-hosted ou planta ficheiros de configuração que são executados da próxima vez que alguém abrir o repositório.
- Comporta-se de forma diferente em CI. O payload de novembro de 2025 verificava
GITHUB_ACTIONS,BUILDKITE,CIRCLE_SHA1e variáveis semelhantes e, em CI, era executado de forma síncrona, terminando o seu trabalho antes de o build prosseguir.
Porque é que o lockfile não basta
O conselho habitual é procurar as versões afetadas em package-lock.json, yarn.lock ou pnpm-lock.yaml. Faça-o primeiro, mas um lockfile limpo não prova que o build esteja limpo:
- Pacotes instalados fora do lockfile.
npx some-toolinstala na cache do npm um pacote que não é dependência do projeto.npm install -gnum passo de configuração, uma ferramenta instalada dentro de um script de build e umnpm installnum Dockerfile contornam todos o lockfile do projeto. - O lockfile de hoje no branch não é o lockfile que foi usado. Um build de há três semanas usou o que o lockfile dizia na altura, no branch que o desencadeou. Os pull requests de atualização de dependências executam a CI com novas versões antes de alguém os rever.
- As caches restauram o que foi instalado antes. Um
node_modulesou uma cache npm restaurados podem conter uma versão que o lockfile atual já não menciona. npm ciinstala o que o lockfile diz. Se a versão infetada já estava fixada no lockfile, uma instalação estrita instala-a fielmente.- Uma versão publicada corretamente pode ainda assim ser maliciosa. A TanStack e o Miasma foram comprometidos através dos seus próprios workflows de lançamento, e as versões infetadas do Miasma tinham proveniência válida. Verificar que uma versão foi publicada corretamente não lhe diz que está limpa.
Aquilo de que realmente precisa é de um registo das versões de pacotes que cada build obteve, em todos os repositórios e em todos os branches, durante todo o período em que uma versão infetada esteve no registo.
Como verificar se foi afetado
- Obtenha a lista de versões afetadas da vaga. Os fornecedores de segurança publicam-nas em poucas horas; a Datadog, por exemplo, mantém um repositório de indicadores de comprometimento com listas de pacotes e versões.
- Pesquise todos os lockfiles, em todos os branches que executaram CI durante a janela, e não apenas no branch predefinido.
- Pesquise os próprios builds. Procure nos registos de CI
setup_bun.js,bun_environment.jsou outros nomes de ficheiros indicados no aviso de segurança, bem como resultados de instalação que mencionem as versões afetadas. Se mantiver SBOM por build, procure nelas as versões. - Verifique a sua organização GitHub, à procura de novos repositórios com o nome do worm, ficheiros de workflow que não escreveu (por exemplo
shai-hulud-workflow.yml,discussion.yamlouformatter_*.yml) e runners self-hosted que não registou. - Limpe as caches. Elimine as caches do GitHub Actions e a cache npm nas máquinas dos programadores e nos runners self-hosted, para que o próximo build instale versões limpas.
- Se algum build instalou uma versão afetada, trate o job como comprometido. Faça a rotação de todos os segredos que este podia ler: tokens npm, tokens GitHub, credenciais cloud e chaves de implementação. Como as vagas posteriores publicaram segredos roubados nas contas de outras vítimas, não assuma que está a salvo por nada ter aparecido na sua.
Como reduzir os danos da próxima vez
- Desative os scripts de instalação sempre que puder. O npm v12, lançado em julho de 2026, desativa os scripts de instalação por predefinição e permite autorizá-los por pacote. O pnpm e o Yarn oferecem controlos semelhantes.
- Espere antes de adotar novas versões. A maioria das versões infetadas é encontrada e removida em horas ou dias. Um período de espera nas atualizações de dependências, suportado tanto pelo Dependabot como pelo Renovate, mantém-nas fora dos seus builds.
- Publique com trusted publishing e tokens de curta duração. O npm revogou todos os tokens clássicos em dezembro de 2025. Use trusted publishing baseado em OIDC a partir da CI em vez de tokens de longa duração e mantenha os workflows de publicação separados dos de build e de teste.
- Reforce os workflows de que o worm abusa. Evite
pull_request_target, fixe as actions por SHA de commit e dê aoGITHUB_TOKENapenas as permissões de que cada job precisa. Consulte segredos do GitHub Actions: como vazam e como os proteger. - Limite os destinos a que os builds se podem ligar. Um build precisa dos seus registos de pacotes e de algumas API. Se só alcançar esses, as chamadas do worm a
webhook.siteou aos seus próprios servidores falham. Consulte como uma política de egress trava a exfiltração a partir do GitHub Actions. Isto não trava a exfiltração através de um host que o build usa legitimamente, como a própria API do GitHub, de que algumas vagas dependeram.
O papel do CRACI
O CRACI executa os seus jobs de GitHub Actions em runners próprios (runs-on: craci), e um proxy que reconhece pacotes no runner regista cada pacote que cada job realmente descarrega do npm e de outros registos, incluindo pacotes restaurados de caches. Esse registo cobre os pontos cegos acima: ferramentas npx, instalações globais e instalações dentro de scripts de build aparecem todas, porque o registo vem do que o job descarregou, e não do que um lockfile declarou.
Quando é anunciada uma nova vaga, procura as versões afetadas nos builds que o CRACI registou, em vez de as reconstruir a partir de lockfiles e registos. As conclusões são agregadas entre builds e repositórios, o inventário do CRACI mostra que versões entraram em que produtos e o traço de rede de cada job mostra a que se ligou, pelo que pode ver se um job que instalou uma versão infetada também alcançou algum sítio onde não devia.
A política de egress do CRACI limita os destinos a que um job se pode ligar, é validada antes de o job começar, falha em modo fechado e alerta-o quando um job tenta alcançar um destino que a política não permite. O CRACI não pontua pacotes por comportamento malicioso; combine-o com uma ferramenta de reputação de pacotes se quiser bloquear versões infetadas antes da instalação.
Marque uma demo para ver o registo de pacotes de um dos seus builds.
