Em 4 de agosto de 2026, alguém comprometeu a conta do mantenedor Jaredwray no npm e publicou versões trojanizadas de keyv, cacheable, flat-cache, file-entry-cache e pelo menos mais seis pacotes do mesmo ecossistema. O ataque é ao vivo enquanto escrevo. A contagem de pacotes afetados passa de 1.280 e sobe a cada hora.

O que faz esse incidente diferente de um comprometimento comum não é o número de pacotes. É que o worm publica versões com assinatura sigstore e provenance legítima do GitHub Actions. Se você audita supply chain por attestation, passa. O malware tem provenance verificável.

A cadeia de ataque em quatro estágios

O time de pesquisa da Socket publicou análise técnica detalhada com reverse engineering completa do payload. O ataque tem quatro estágios encadeados, e cada um resolve um problema específico de evasão.

Estágio 1: preinstall hook. O package.json trojanizado adiciona "preinstall": "node setup.mjs". O código da biblioteca (dist/) é byte-idêntico ao build legítimo por SHA-256. O pacote funciona normalmente depois de instalado. O comportamento malicioso roda antes do install terminar, no lifecycle do npm.

Estágio 2: runtime Bun. setup.mjs baixa um Bun standalone (sem checksum, sem verificação de assinatura), detecta plataforma e arquitetura (incluindo Alpine/musl), e executa o segundo estágio com Bun em vez de Node. Isso bypassa controles que monitoram apenas processos Node.

Estágio 3: coleta de credenciais. O payload (Math_Symbol.js, 728 KB empacotado com Bun) faz sweep de credenciais no estilo TruffleHog e consulta ativa de metadata services:

  • AWS IMDS (169.254.169.254), ECS task metadata (169.254.170.2)
  • HashiCorp Vault tokens (~/.vault-token, /run/secrets/VAULT_TOKEN)
  • Kubernetes service account tokens (/var/run/secrets/kubernetes.io/...)
  • npm tokens (via registry whoami e endpoints de token)
  • GitHub Actions OIDC e GITHUB_TOKEN (via environment, arquivos, e scanning de processo no runner)
  • GCP service account keys, Azure client secrets

Strings protegidas com basE91 polimórfico: um opcode table numérico alimenta dezenas de alfabetos por escopo, decodificados lazy. Recuperar os strings exige reimplementar basE91 e bruteforcear cada alfabeto.

Estágio 4: propagação worm. Para cada pacote do mantenedor descoberto via registry.npmjs.org/-/v1/search?text=maintainer:, o payload baixa o tarball, injeta o mesmo hook, recalcula integrity e shasum, bumpa a versão, e publica via PUT no registry usando o token roubado. Um componente dedicado constrói DSSE attestation envelopes, solicita certificados Fulcio, e submet entradas Rekor. As versões republicadas chegam com provenance fresca e verificável.

Não é um worm que espalha pacotes suspeitos. É um worm que espalha pacotes assinados.

Por que keyv e cacheable importam

keyv é uma abstração de key-value store com adapters para Redis, SQLite, Postgres, MongoDB. flat-cache e file-entry-cache são bibliotecas de cache profundas na árvore de dependências do ESLint. Juntas, esses pacotes somam dezenas de milhões de downloads semanais.

A cadeia comum é eslintfile-entry-cacheflat-cachekeyv. A maioria dos afetados nunca instalou nenhum desses pacotes diretamente. Estão expostos por dependência transitiva, e o preinstall roda no npm install do pacote pai sem que o desenvolvedor saiba que ele existe.

O que muda na ideia de provenance

A lição central do incidente, que o torna mais significativo que ondas anteriores do mesmo worm (TanStack em maio, AntV em junho), é uma distinção que a indústria de supply chain security ainda não internalizou: provenance atesta integridade de build, não integridade de source.

O pipeline npm/sigstore fez exatamente o que foi designado para fazer. Produziu uma attestation assinada e verificável para malware, porque o source que ele buildou já estava trojanizado. O atacante fez force push na main do repo, deletou e recriou tags, e cortou release via GitHub Actions. A provenance reflete o processo de build, e o processo de build foi legítimo. O código-fonte que entrou nele não era.

Isso derrota SLSA, derrota sigstore, derrota qualquer verificação que confie na chain de build sem inspecionar o conteúdo do que está sendo buildado. Se o modelo de ameaça do seu pipeline de supply chain assume que “provenance verificável = seguro”, esse ataque é o counterexample.

Persistência além do npm

O payload planta dois vetores de persistência que não dependem de npm install:

Autostart em IDEs e agentes de IA. O repo de source recebe hooks em .claude/settings.json (SessionStart) e .vscode/tasks.json (folderOpen). Quem clona o repo e abre no VS Code ou num agente de coding que suporte Claude executa o loader automaticamente, sem instalar nada.

Dead-man’s switch em nível de host. O payload escreve o token GitHub roubado e um handler command em ~/.config/gh-token-monitor/, e instala um LaunchAgent no macOS (com.user.gh-token-monitor) ou systemd user service no Linux com loginctl enable-linger. Um watcher script polled o token a cada 60 segundos. Quando o token para de funcionar (HTTP 4xx, no momento exato em que é rotacionado ou revogado), ele avalia um handler string fornecido remotamente, deleta seu state, e sai. Self-clear após 24h de TTL.

O nome do serviço (“GitHub Token Validity Monitor”) parece conveniência de developer. Nenhum linter, secret scanner, ou release tool instala um background service que espera sua própria credencial ser revogada para então executar um comando remoto. Esse é o artefato que remove qualquer ambiguidade sobre intenção.

Linha do tempo

Todos os tempos UTC, 4 de agosto de 2026, baseados em timestamps do registry npm:

  • 09:30-09:32: Pacotes scoped @keyv/* publicam tarballs 6.0.0 (sem o hook, mas suspeitos pela conta comprometida)
  • 09:35: keyv@6.0.0 publicado, primeira versão com o preinstall hook malicioso
  • 09:38: Socket detecta o hook malicioso ~6 minutos após publicação
  • 09:39: Force pushes na main do repo jaredwray/keyv, tags deletadas e recriadas
  • 10:09-10:14: Burst da família cacheable: cacheable@2.5.1, flat-cache@6.1.24, cacheable-request@13.0.20, file-entry-cache@11.1.6, cache-manager@7.2.10, e os scoped @cacheable/*

A janela entre publicação e detecção foi de 6 minutos. Qualquer npm install nesse intervalo puxou o payload.

O contexto das ondas anteriores

Este é o mesmo conjunto de técnicas documentado em ondas anteriores do Shai-Hulud. A primeira onda comprometeu 100+ pacotes. A “Mini Shai-Hulud” atingiu o ecossistema TanStack em maio de 2026 (160+ pacotes), forçando a OpenAI a rotacionar certificados de code signing de apps macOS. Uma terceira onda atingiu o ecossistema AntV da Alibaba.

Esta onda é diferente em escala de blast radius. As ondas anteriores atingiram ecossistemas específicos (TanStack, AntV). Keyv e cacheable são dependências transitas do ESLint, que está em praticamente todo projeto JavaScript do mundo. O número de ambientes potencialmente expostos é ordens de magnitude maior.

O que fazer agora

Para desenvolvedores: pinne cada pacote afetado na versão imediatamente anterior à listada, reconstrua lockfiles, e prefira versões exatas locked por integrity hash. Não permita caret/tilde ranges enquanto a conta do mantenedor estiver comprometida. Onde possível, bloquee o scope inteiro keyv, @keyv, e cacheable no proxy de registry ou allowlist.

Para times de segurança: antes de rotacionar qualquer credencial, procure e remova o dead-man’s switch. Revogação é o gatilho dele. Verifique:

  • ~/.local/bin/gh-token-monitor.sh
  • ~/.config/gh-token-monitor/ (token, handler, started_at)
  • ~/Library/LaunchAgents/com.user.gh-token-monitor.plist (macOS; também descarregue o LaunchAgent)
  • ~/.config/systemd/user/gh-token-monitor.service (Linux; também loginctl disable-linger)
  • /tmp/gh-token-monitor.{out,err}.log
  • .claude/settings.json e .vscode/tasks.json nos seus repos

Depois de remover o switch e o implant, rotacione todas as credenciais alcançáveis do host: npm tokens, GitHub PATs e GITHUB_TOKEN, AWS, GCP, Azure, Vault, Kubernetes, secrets de CI. Revoke em vez de apenas rotacionar tokens npm e GitHub. Depois audite a conta npm por versões de pacote publicadas hoje e o GitHub por repos recém-criados e commits inesperados.

Para qualquer pipeline CI que roda npm install ou npm ci: adicione --ignore-scripts como default. O payload roda no lifecycle de preinstall. Se scripts de install não são necessários no seu CI (na maioria dos casos não são), desabilitar globally elimina essa classe de vetor.

O que eu ainda não sei

  1. A contagem final de pacotes afetados. O incidente está ativo, os números só sobem.
  2. Se o atacante manteve acesso ao repo após as primeiras publicações, e se há versões trojanizadas publicadas fora da janela detectada pela Socket.
  3. Quantos ambientes de CI executaram o payload antes da detecção. A janela foi de 6 minutos no primeiro pacote, mas o burst da família cacheable veio 34 minutos depois, sugerindo que o atacante teve tempo de refinar.

Fontes