Mais 131% CVE comunicadas por dia em 2026 do que no ano passado.
← Todas as vulnerabilidades

Shai-Hulud: o worm do npm e como verificar se os seus builds foram afetados

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

QuandoVagaO 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. A CISA publicou um alerta a 23 de setembro.
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. Registada como CVE-2026-45321, adicionada ao catálogo KEV da CISA a 27 de maio.
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.
Setembro de 2026RessurgimentoA 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 2026tensorlakeA 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:

  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.

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, postinstall e prepare, 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 script prepare executava o payload e depois falhava de propósito, pelo que o npm a descartava e não ficava rasto em node_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

  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.

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

  1. 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.
  2. 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
  1. Fixe uma versão limpa e remova os workflows, repositórios, runners e ficheiros de configuração que o worm adicionou.

Verificar a limpeza

  1. Todos os lockfiles resolvem para versões limpas. Em cada branch, npm ls <package> (ou pnpm why ou yarn why) mostra a versão instalada; nenhuma pode constar da lista da vaga.
  2. 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
  1. 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.
  2. As caches desapareceram. gh cache list não mostra nenhuma cache criada antes da limpeza em nenhum repositório afetado.
  3. 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.
  4. 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ê 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.

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:

VagaIndicadoresFonte
Setembro de 2025Payload 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.siteStepSecurity, CISA
Novembro de 2025setup_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 SHA1HULUDWiz
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.moeGHSA-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:

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.

Perguntas frequentes

O Shai-Hulud continua ativo?

Sim. Tem regressado em vagas desde setembro de 2025, e os seus autores publicaram o código-fonte em maio de 2026, pelo que as vagas posteriores podem vir de grupos diferentes. A vaga mais recente aqui abordada atingiu o pacote tensorlake a 8 de outubro de 2026.

O Shai-Hulud tem uma CVE?

O worm no seu conjunto não tem nenhuma. O GitHub publica avisos de malware para pacotes infetados individuais, e a vaga TanStack de maio de 2026 tem a CVE-2026-45321, que a CISA adicionou ao seu catálogo Known Exploited Vulnerabilities a 27 de maio de 2026.

Um lockfile limpo significa que não fomos afetados?

Não. Pacotes instalados com npx, instalações globais, instalações dentro de scripts de build e Dockerfiles, lockfiles mais antigos noutros branches e caches restauradas podem todos trazer uma versão infetada que o lockfile atual não mostra. Precisa de um registo do que cada build realmente obteve.

Como verifico se fui afetado?

Obtenha as versões de pacotes afetadas pela vaga, pesquise todos os lockfiles em todos os branches que executaram CI durante a janela, pesquise os registos de CI e as SBOM por build à procura das versões e dos nomes dos ficheiros do payload, e verifique a sua organização GitHub à procura de repositórios, workflows e runners self-hosted que não criou.

O que faço se um build instalou uma versão infetada?

Trate o job como comprometido. Faça a rotação de todos os segredos que podia ler, incluindo tokens npm, tokens GitHub, credenciais cloud e chaves de implementação, elimine as caches do GitHub Actions e as caches npm e procure workflows, repositórios e runners que o worm tenha adicionado.

Como sei se o worm foi executado nos nossos builds?

Procure nos registos de build, nos lockfiles e nos pacotes instalados os indicadores da vaga: ficheiros do payload como bundle.js, setup_bun.js, bun_environment.js ou router_init.js, repositórios e workflows que não criou, um runner self-hosted chamado SHA1HULUD e ligações a webhook.site ou getsession.org. Qualquer job que tenha instalado uma versão afetada executou o payload, por isso trate-o como comprometido mesmo que não encontre mais nada.