Revogar o token era o gatilho
Em 8 de outubro, o tensorlake 0.5.144 chegou ao npm com um worm Shai-Hulud dentro. Ele rouba credenciais do GitHub, do npm, da nuvem e de SSH, planta .claude/settings.json nos repositórios da vítima e instala um monitor que verifica o token do GitHub roubado a cada 60 segundos e executa rm -rf ~/ no momento em que ele para de funcionar. Revogar o token, o primeiro passo do roteiro padrão de resposta a incidentes, apaga o diretório pessoal da vítima. No Bromure Agentic Coding, a barreira de idade padrão esconde essa versão do npm, e o token do GitHub dentro da máquina nunca foi o verdadeiro.
O worm deixou no código uma frase para quem a encontrasse: “IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner”. Se você revogar este token, o computador do dono será apagado. O código faz o que a frase diz.
À 01:12 UTC de quarta-feira, 8 de outubro, o workflow de release de
tensorlakeai/tensorlake publicou a versão 0.5.144 do SDK tensorlake no npm.
A Tensorlake é uma plataforma serverless de sandboxes para agentes de IA, e seu
pacote npm tem cerca de 12.000 downloads por semana. A nova versão veio do
próprio repositório do projeto, gerada pelo próprio workflow do projeto, e o
npm exibia para ela um atestado de procedência.
O Socket a sinalizou onze minutos depois. StepSecurity, Aikido e The Hacker News vieram em seguida no mesmo dia. A versão traz uma nova variante do worm Shai-Hulud, e esta vem com um refém.
Como ele entrou
A StepSecurity reconstruiu a linha do tempo. À 01:20 UTC de 7 de outubro, alguém fez push de um commit direto na branch principal do repositório, com o nome de um mantenedor. Mais sete commits vieram nas horas seguintes. Ninguém os percebeu, e um dia depois o workflow de release compilou o código da main e o publicou. O atestado de procedência estava correto. Ele provava onde o pacote tinha sido gerado, e esse lugar já estava nas mãos do atacante.
O package.json do pacote ganhou uma linha:
"preinstall": "node lib/setup.mjs"
O npm executa preinstall antes mesmo de o pacote ser desempacotado no lugar.
O script baixa o runtime Bun e o usa para executar lib/Math_Symbol.js, um
payload ofuscado de 856 KB. Em CI, ele não faz nada. O resumo da StepSecurity:
“o alvo são as máquinas dos desenvolvedores”.
O que ele leva, e para onde vai
O payload coleta tokens do GitHub e do npm, chaves de nuvem da AWS, do Azure e
do GCP, segredos do Kubernetes e do HashiCorp Vault, chaves SSH, arquivos
.env, logins salvos no navegador, carteiras de criptomoedas e a configuração
de ferramentas de programação com IA: .claude, .cursor, .kiro, Windsurf e
Zed. Ele criptografa o butim e o envia para iseekaigogo.com ou, se isso
falhar, para um repositório público que cria na própria conta do GitHub da
vítima, com a descrição “Shai-Hulud: Here We Go Again.” Quando o domínio cai,
ele procura um novo em um contrato Ethereum, por meio de cerca de 30 endpoints
RPC públicos.
Depois, ele se espalha. Com o token do npm da vítima, baixa cada um dos pacotes
dela, se adiciona, sobe a versão e publica de novo. Com o token do GitHub, faz
commit de .claude/settings.json e .vscode/tasks.json nos repositórios da
vítima, como [email protected], com a mensagem
chore: update dependencies. O próximo desenvolvedor que abrir um desses
projetos no Claude Code ou no VS Code executa o worm de novo. Como disse Ashish
Kurmi, da StepSecurity, o malware “roda de novo quando alguém abre o projeto no
Claude Code ou no VS Code”.
O refém
A novidade é um serviço chamado gh-token-monitor. No Linux, é uma unidade de
usuário do systemd em ~/.config/systemd/user/; no macOS, um LaunchAgent em
~/Library/LaunchAgents/com.user.gh-token-monitor.plist; no Windows, uma
tarefa de logon que executa monitor.ps1. A StepSecurity descreve o que ele
faz:
A cada 60 segundos, por até 24 horas, o serviço verifica esse token na API do GitHub. Se o GitHub o rejeitar, o serviço executa
rm -rf ~/.
Imagine que você é a vítima. Você descobre que um pacote que instalou roubou
seu token do GitHub. O roteiro padrão diz para revogá-lo na hora. Você revoga,
e em menos de um minuto seu diretório pessoal sumiu: seus checkouts, seu
trabalho sem commit, seus dotfiles e as evidências do que o worm fez. O Socket
encontrou a ameaça escrita no código como uma string literal:
IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner.
Por isso os alertas agora vêm com uma regra de ordem. O passo 3 da StepSecurity é encontrar e remover o monitor, e antes fazer backup de tudo o que você não pode perder. O aviso, em negrito: “Não revogue nenhum token do GitHub antes de concluir o passo 3.” O Socket diz o mesmo. Cada hora que você passa caçando o monitor é uma hora em que o atacante tem um token válido com suas permissões de push.
A mesma instalação em uma máquina Bromure
No Bromure Agentic Coding, o Claude Code, o Codex e os outros agentes rodam em
uma VM Linux no seu Mac, uma VM por máquina. Toda conexão que a VM abre,
inclusive cada download do npm, passa por um proxy no Mac. Reproduza o 8 de
outubro em uma dessas máquinas: o agente adiciona tensorlake a um projeto e
executa npm install.
A versão ainda não existia
Toda máquina nova vem com a barreira de idade da cadeia de suprimentos
ligada, com mínimo de dois dias. Quando o npm pede ao registro a lista de
versões do tensorlake, o proxy remove toda versão publicada há menos de dois
dias e faz latest apontar para a mais nova que já tem idade suficiente. Em
8 de outubro, era a 0.5.143. O npm a instalou e seguiu em frente. De dentro da
máquina, a 0.5.144 nunca existiu, e quando ela teria passado do corte, já tinha
sumido do npm.
Se um lockfile ou o agente pedisse a 0.5.144 pelo nome, o proxy recusaria o download com um HTTP 451 e um motivo que o npm imprime do jeito que está:
Bromure Supply-Chain Security blocked this request: npm package
[email protected] published 3 hours ago — policy requires 2 days minimum
O agente também lê essa linha, e fixa a versão anterior por conta própria.
Mais duas camadas ficam no mesmo painel Supply Chain. Com o socket.dev
selecionado e Block compromised packages ligado, o download é recusado
assim que o socket.dev sinaliza o pacote, o que levou onze minutos para o
Socket em 8 de outubro. Com Strip preinstall / install / postinstall /
prepare from npm tarballs on the fly ligado, o proxy reescreve o tarball e a
linha "preinstall": "node lib/setup.mjs" some antes de o npm vê-la. Essa linha
é a única porta de entrada do worm.
Digamos que ele rode mesmo assim
Tire todas as camadas e deixe o payload rodar. Ele roda como o usuário da VM, no diretório pessoal da própria máquina, e sai procurando o que está na sua lista.
Os tokens são substitutos. Nenhum arquivo, variável de ambiente ou
processo da VM contém um token real do GitHub, um segredo de nuvem ou uma chave
privada SSH. GH_TOKEN e ~/.git-credentials contêm um token ghp_ com o
formato certo, e o proxy no Mac só coloca o verdadeiro quando uma requisição
sai para o GitHub. ~/.ssh não tem chave privada: SSH_AUTH_SOCK é uma ponte
para um ssh-agent no Mac, que assina e nunca entrega a chave. O diretório
pessoal do seu Mac, as Chaves dele e os perfis de navegador não estão na VM de
jeito nenhum. O worm criptografa e envia um pacote de substitutos que o GitHub,
a AWS e o Kubernetes rejeitam em qualquer lugar que não seja através desse
proxy.
A saída passa pelo Mac. Uma máquina com Unmatched traffic em Deny
no painel Guardrails, com regras para o provedor de modelos, os registros
e o GitHub, nunca se conecta a iseekaigogo.com nem a nenhum dos 30 endpoints
RPC do Ethereum. Seja qual for a configuração, cada uma dessas conexões aparece
na Security Timeline, que está sempre ligada em todas as máquinas.
As escritas esperam um clique. Máquinas novas usam Prompt before write
para toda forja git. O plano B do worm no GitHub cria um repositório público na
sua conta: isso é um POST /user/repos, uma escrita, então o macOS mostra
Allow write on "github.com" from workspace "…"? com a requisição como ela
apareceu na rede. A propagação faz commit de .claude/settings.json nos seus
repositórios e dá push: o mesmo diálogo, desta vez com
POST /acme/app.git/git-receive-pack. Você não pediu ao agente para criar um
repositório chamado “Shai-Hulud: Here We Go Again.” Você clica em Don't
allow, e a propagação do worm termina no seu Mac.
O refém é um substituto
A unidade gh-token-monitor se instala mesmo assim no
~/.config/systemd/user/ da VM e começa a chamar api.github.com/user a cada
60 segundos com o token que encontrou. Esse token é o substituto. O proxy
coloca o verdadeiro para o GitHub, o GitHub responde 200 e o monitor espera.
Isso inverte a ordem da resposta. Em um notebook, o token real está nas mãos do
atacante, e a corrida é revogá-lo antes que ele o use, sem disparar a limpeza.
No Bromure, o token real nunca entrou na VM, então o atacante não tem com o que
apostar corrida com você. Nada fora do seu proxy aceita o valor ghp_ que ele
levou.
Desligue a máquina, e o monitor para junto com ela. Abra Rewind home… e
escolha o checkpoint do boot anterior à instalação: o Bromure tira um a cada
boot bem-sucedido. O arquivo da unidade, o script em ~/.local/bin e os
arquivos .claude/settings.json plantados nos checkouts da máquina voltam
todos ao que eram. O diretório pessoal como estava um instante antes é guardado
primeiro como um novo ponto de reversão, então o próprio retrocesso pode ser
desfeito. Para começar do zero, Reset disk clona de novo o disco do sistema
a partir da imagem base assinada.
E se o gatilho disparar mesmo assim, rm -rf ~/ apaga /home/ubuntu dentro de
uma única VM. O diretório pessoal do seu Mac está fora do alcance dele, e
Rewind home… traz de volta o da máquina a partir do último checkpoint.
Quem instalou?
A primeira pergunta em um incidente como este é se alguma máquina da equipe baixou a versão ruim. Em um notebook, você vasculha lockfiles, caches do npm e o histórico do shell, e torce para que o worm não tenha apagado os rastros.
No Bromure, cada download de pacote é uma linha na Security Timeline, esteja
alguma camada de cadeia de suprimentos ligada ou não:
npm · [email protected], com a máquina, a hora e a decisão. As linhas ficam
guardadas no Mac por 90 dias, onde nada dentro de uma VM consegue editá-las.
Filtre por Supply chain, procure a versão e você tem a resposta.
Export… salva o resultado como CSV para o ticket do incidente. Em Macs
inscritos no bromure.io, o mesmo download vai para o fluxo de eventos da
organização como um evento supply_chain.fetch, então uma equipe de segurança
pode fazer a pergunta uma vez só para todos os desenvolvedores.
A versão ruim nunca apareceu
A barreira de idade, ligada por padrão com mínimo de dois dias, remove
versões recentes da lista de versões do npm. Em 8 de outubro,
npm install tensorlake resolveu para a 0.5.143.
O token roubado era um substituto
As credenciais do GitHub, do npm, da nuvem e de SSH ficam no Mac. O refém
do worm era um valor ghp_ que só funciona através do proxy do Bromure.
Para se espalhar, precisa do seu clique
Criar um repositório e dar push em um commit são escritas. Com Prompt before write, o padrão em máquinas novas, os dois esperam um diálogo do macOS que a VM não consegue responder.
A limpeza atinge uma VM
rm -rf ~/ alcança o diretório pessoal da máquina, não o seu. O Rewind
home o restaura a partir do checkpoint tirado no último boot.
O próximo roteiro
O Shai-Hulud voltou várias vezes este ano, e seus autores moldam cada versão de acordo com o jeito como os defensores lidaram com a anterior. Os defensores revogavam tokens em poucas horas, então esta versão pune a revogação. A próxima vai punir outra coisa: rodar um script de limpeza, apagar um LaunchAgent, abrir o projeto de novo.
Em um notebook, cada uma dessas reviravoltas funciona porque o atacante tem algo real na mesma máquina que você está tentando limpar. Em uma máquina Bromure, as credenciais reais, seu diretório pessoal e o registro do que aconteceu ficam todos no Mac, atrás do proxy. Limpar é voltar no tempo, e você pode fazer isso antes de rotacionar qualquer coisa. Para saber mais sobre a fronteira de rede por trás disso, leia A instalação estava limpa e Quarenta mil dólares por fuga.