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

QuandoNomeO que foi diferente
Setembro de 2025Shai-HuludUm 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 2025Sha1-Hulud: The Second ComingPassou 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 2025Teste "3.0"Uma versão modificada num único pacote, sem propagação relevante.
Maio de 2026TanStack84 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 2026AntV639 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 2026Miasma32 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 2026CHAINDROPPacotes 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:

  1. É executado na instalação. Um script preinstall ou postinstall é executado assim que npm install ou npm ci chega à versão infetada. Nada no seu código tem de o importar.
  2. 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.
  3. 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.
  4. 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.
  5. Comporta-se de forma diferente em CI. O payload de novembro de 2025 verificava GITHUB_ACTIONS, BUILDKITE, CIRCLE_SHA1 e 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-tool instala na cache do npm um pacote que não é dependência do projeto. npm install -g num passo de configuração, uma ferramenta instalada dentro de um script de build e um npm install num 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_modules ou uma cache npm restaurados podem conter uma versão que o lockfile atual já não menciona.
  • npm ci instala 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

  1. 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.
  2. Pesquise todos os lockfiles, em todos os branches que executaram CI durante a janela, e não apenas no branch predefinido.
  3. Pesquise os próprios builds. Procure nos registos de CI setup_bun.js, bun_environment.js ou 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.
  4. 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.yaml ou formatter_*.yml) e runners self-hosted que não registou.
  5. 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.
  6. 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ê ao GITHUB_TOKEN apenas 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.site ou 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.