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. Se algum dos seus builds instalou uma versão infetada, trate esse job como comprometido e faça a rotação de todos os segredos que este podia ler.
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, e as restantes sobretudo 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. Por isso, quando é anunciada uma nova vaga, todas as equipas fazem a mesma pergunta: algum dos nossos builds instalou uma versão infetada?
O que aconteceu
| Quando | Vaga | 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. A CISA publicou um alerta a 23 de setembro. |
| 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. Registada como CVE-2026-45321, adicionada ao catálogo KEV da CISA a 27 de maio. |
| 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. |
| Setembro de 2026 | Ressurgimento | A 7 de setembro, o payload voltou em quatro pacotes que passaram pela nova análise de malware do npm no momento da publicação. |
| Outubro de 2026 | tensorlake | A 8 de outubro, [email protected] foi publicado com o worm. A OX Security detetou novas chaves do atacante e sugere "um agente de ameaça independente ou um novo grupo que usa o nome Shai-Hulud". |
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.
Quem é afetado
Qualquer pessoa cujas máquinas ou builds tenham instalado uma versão infetada enquanto esta esteve no registo: portáteis de programadores, jobs de CI, builds Docker que executam npm install e runners self-hosted. Não precisa de depender diretamente do pacote; basta uma dependência transitiva ou uma ferramenta executada com npx. Cada vaga tem a sua própria lista de pacotes e versões, e as versões infetadas são normalmente removidas em horas ou dias, pelo que o que importa é se algo as instalou durante essa janela.
Como funciona
O Shai-Hulud não é um erro do npm. É código malicioso publicado dentro de pacotes de resto legítimos, que os avisos de malware do GitHub para os pacotes infetados classificam como código malicioso incorporado (CWE-506). Cada vaga combina algumas técnicas:
- Execução durante a instalação. O npm executa os scripts de ciclo de vida de um pacote, como
preinstall,postinstalleprepare, durante a instalação, com as permissões do utilizador ou do job de CI. Basta que uma versão infetada esteja na árvore de dependências; nada tem de a importar. - Propagação com direitos de publicação roubados. Com um token npm ou GitHub retirado a uma vítima, o worm publica novas versões dos outros pacotes dessa vítima com o payload acrescentado, pelo que cada maintainer infetado infeta mais pacotes.
- Publicação através do próprio pipeline de lançamento da vítima (TanStack, maio de 2026). Segundo o aviso do GitHub, o atacante encadeou um workflow
pull_request_target, uma cache do GitHub Actions envenenada através da fronteira de confiança entre o fork e o repositório base, e um token OIDC lido da memória do runner, e publicou através do trusted publishing da TanStack sem alterar o workflow de publicação. As versões infetadas declaravam uma dependência opcional de um commit num fork, cujo scriptprepareexecutava o payload e depois falhava de propósito, pelo que o npm a descartava e não ficava rasto emnode_modules. - Proveniência válida (Miasma, junho de 2026). Workflows injetados publicaram a partir dos próprios repositórios das vítimas, pelo que as versões infetadas tinham proveniência válida, e um payload cifrado de forma única em cada infeção contornava a deteção baseada em hashes.
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.
Comandos que pode adaptar. Para pesquisar os lockfiles de todos os branches remotos, coloque as versões afetadas da vaga em affected.txt, uma name@version por linha:
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
Para listar os runners self-hosted da sua organização, os novos repositórios que mencionam o worm e os ficheiros de workflow que se sabe que o worm cria (substitua 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
Os repositórios também podem ter runners próprios (gh api repos/ORG/REPO/actions/runners), e as primeiras vagas criaram repositórios nas contas pessoais dos programadores cujos tokens roubaram, por isso peça aos seus programadores que verifiquem as deles.
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. Consulte fomos afetados? como avaliar uma nova CVE em todos os seus builds.
Como corrigir, e o que fazer se ainda não puder aplicar o patch
- 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.
- 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:
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
- Fixe uma versão limpa e remova os workflows, repositórios, runners e ficheiros de configuração que o worm adicionou.
Verificar a limpeza
- Todos os lockfiles resolvem para versões limpas. Em cada branch,
npm ls <package>(oupnpm whyouyarn why) mostra a versão instalada; nenhuma pode constar da lista da vaga. - Uma instalação limpa não contém ficheiros do payload. Depois da limpeza das caches acima, instale novamente e procure os ficheiros do payload indicados nos avisos:
find node_modules -name bundle.js -size +3M \
-o -name setup_bun.js -o -name bun_environment.js -o -name router_init.js
- As credenciais antigas já não funcionam. Os tokens npm revogados já não aparecem em
npm token list, os tokens GitHub e as chaves de implementação antigos foram eliminados e as chaves cloud foram desativadas, e não apenas substituídas. - As caches desapareceram.
gh cache listnão mostra nenhuma cache criada antes da limpeza em nenhum repositório afetado. - Não resta nada do que o worm acrescentou: nenhum repositório, ficheiro de workflow, runner self-hosted ou ficheiro de configuração de editores e assistentes de IA que não tenha criado.
- O próximo build está limpo. Verifique as versões que realmente instalou, a partir do respetivo registo ou da SBOM por build, e não apenas do lockfile.
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. A package firewall do GitHub, em pré-visualização pública para o GitHub Enterprise Cloud desde 20 de outubro de 2026, verifica as instalações npm à procura de pacotes maliciosos e também oferece períodos de espera.
- 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.
Verificar se houve comprometimento
O Shai-Hulud é malicioso por conceção: qualquer máquina ou job que instalou uma versão infetada executou-o. Os indicadores variam consoante a vaga, por isso use o aviso da vaga que está a verificar. De fontes primárias:
| Vaga | Indicadores | Fonte |
|---|---|---|
| Setembro de 2025 | Payload bundle.js, SHA-256 46faab8ab153fae6e80e7cca38eab363075bb524edd79e42269217a083628f09; workflow .github/workflows/shai-hulud-workflow.yml num branch shai-hulud; um repositório público chamado Shai-Hulud; ligações a webhook.site | StepSecurity, CISA |
| Novembro de 2025 | setup_bun.js, SHA-1 d1829b4708126dcc7bea7437c04d1f10eacd4a16; bun_environment.js, SHA-1 d60ec97eea19fffb4809bc35b91033b52490ca11 ou 3d7570d14d34b0ba137d502f042b27b0f37a59fa; repositórios com cloud.json, contents.json, environment.json, truffleSecrets.json e actionsSecrets.json; workflows .github/workflows/discussion.yaml e formatter_*.yml; um runner self-hosted chamado SHA1HULUD | Wiz |
| Maio de 2026, TanStack | "@tanstack/setup": "github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c" em optionalDependencies; payload router_init.js (cerca de 2,3 MB) na raiz do pacote; auxiliar tanstack_runner.js; ligações a filev2.getsession.org e de seed1.getsession.org a seed3.getsession.org, e descargas de segunda fase a partir de litter.catbox.moe | GHSA-g7cv-rxg3-hmpx |
As vagas posteriores alteravam o payload em cada infeção, pelo que um hash de ficheiro que não coincide não prova nada; compare primeiro as versões dos pacotes.
Quando presumir o comprometimento. Trate como comprometidos todos os hosts e jobs de CI que instalaram uma versão afetada, mesmo que a instalação tenha falhado ou não tenha deixado nada em node_modules. O aviso do GitHub sobre a TanStack diz que qualquer ambiente de desenvolvimento ou de CI que tenha instalado uma versão afetada "deve ser considerado comprometido". Verifique a partir da primeira publicação da versão infetada, e não a partir do dia em que a vaga foi anunciada.
Faça a rotação das credenciais. Faça a rotação de tudo o que a instalação podia alcançar. Na vaga TanStack, isso significou credenciais AWS, GCP, Kubernetes, Vault, GitHub, npm e SSH, segundo o post-mortem. Reveja os registos de auditoria da cloud à procura de atividade dos hosts afetados durante e após a janela de instalação.
Limpe caches e artefactos. Procure nos repositórios de artefactos e nos proxies de dependências cópias em cache das versões afetadas e elimine-as, como aconselha a CISA. Elimine node_modules, a cache npm e as caches de CI; na vaga TanStack, a porta de entrada do atacante foi uma cache envenenada da store do pnpm, por isso verifique as caches do Actions além das caches de pacotes. Reconstrua e volte a lançar tudo o que um job comprometido tenha produzido.
O que dizer aos seus clientes
Se entrega software construído com npm, os clientes vão perguntar se o seu produto contém o worm ou se o seu pipeline de lançamento foi comprometido. Responda com o que verificou e o que fez:
Verificámos todos os builds do [produto] entre [data] e [data] em relação às listas publicadas de versões de pacotes npm infetadas pelo Shai-Hulud. [Nenhum build instalou uma versão afetada.] / [Os builds do [componente] de [data] instalaram [pacote@versão]; fizemos a rotação das credenciais a que esses builds podiam aceder, reconstruímos o [produto] a partir de dependências limpas e lançámos a [versão]. As versões [x a y] são afetadas; atualize para a [versão].] Atualizaremos este aviso se novas vagas alterarem a situação.
Se ainda estiver a verificar, diga-o, e diga quando vai responder. Para uma primeira declaração e modelos para cada resultado, consulte como responder aos clientes após um ataque à cadeia de abastecimento.
Como o CRACI ajuda
O CRACI executa os seus jobs de GitHub Actions em runners próprios (runs-on: craci) e 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, e o traço de rede de cada job mostra a que se ligou. O histórico de builds guarda todos os builds anteriores com a respetiva SBOM, pelo que vê que builds continham uma versão infetada e a janela em que esteve exposto, dentro do seu período de retenção.
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. As versões infetadas com uma CVE publicada, como a vaga TanStack, são assinaladas como qualquer outra vulnerabilidade, e os policy gates podem travar builds que as contenham. O CRACI não pontua pacotes por comportamento malicioso e não assinala uma nova versão infetada antes de existir um aviso de segurança; combine-o com uma ferramenta de reputação de pacotes se quiser bloquear versões infetadas antes da instalação.
Veja o registo de pacotes de um dos seus próprios builds
Outros incidentes: todas as vulnerabilidades.
Fontes e atualizações
Fontes primárias e investigação original:
- CISA: alerta de 23 de setembro de 2025, catálogo KEV
- GitHub: GHSA-g7cv-rxg3-hmpx (CVE-2026-45321), o nosso plano para uma cadeia de abastecimento npm mais segura, tokens clássicos do npm revogados
- Postmortem da TanStack
- A Wiz sobre a vaga de novembro de 2025 e as suas consequências
- A Microsoft sobre a AntV e as orientações de deteção da Microsoft
- A Datadog e a Elastic sobre o CHAINDROP
- A StepSecurity sobre a vaga de setembro de 2025
- FIRST: pontuação EPSS da CVE-2026-45321
Atualizações:
- 2026-11-11: Acrescentados à caixa o EPSS, a classe CWE e o crédito, verificações prontas a copiar e secções sobre como funciona o worm, a verificação da limpeza e a verificação de comprometimento, com indicadores por vaga.
- 2026-11-11: Movido do blogue para a secção de vulnerabilidades. Adicionadas a vaga tensorlake de 8 de outubro de 2026, a data do ressurgimento de setembro de 2026, a CVE-2026-45321 e a sua inclusão no KEV para a vaga TanStack, e a package firewall do GitHub.
- 2026-09-25: Primeira publicação no blogue do CRACI.