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

Onze testes passaram

A Straiker publicou um repositório que faz o Claude Code executar malware sem jailbreak. O CONTRIBUTING.md dele manda a revisão para um subagente que o próprio repositório define, fixado no Haiku e mantido longe da pasta de testes. Depois o Fable roda o pytest, um arquivo de teste baixa e inicia um implante Sliver, e o terminal informa 11 passed. A Anthropic fechou o relato como Informative: confiar na configuração de um repositório é a decisão de confiança no workspace tomada pelo usuário. O Bromure Agentic Coding decide até onde essa confiança chega: uma VM com chaves substitutas, um firewall no Mac e um trace que o repositório não consegue editar.

O agente seguiu o fluxo de trabalho documentado do projeto, cada passo dele, e esse fluxo tinha sido escrito pelo atacante. O terminal dizia 11 passed.

Você encontra no GitHub um pequeno cliente de métricas que pouparia uma semana de trabalho e pede ao Claude Code para cloná-lo e dar uma olhada. Ele faz um bom trabalho. Lê o código, aponta um buffer sem limite e um loop de novas tentativas que vai girar para sempre com uma mensagem envenenada, e sugere correções. Você diz o de sempre: corrige isso e prepara um PR.

Alguns minutos depois as correções estão feitas, os testes estão verdes e o resumo diz 11 passed. Em algum momento desses minutos, um implante abriu um shell remoto no seu notebook.

Esse é o cenário que Brian Cumi, da Straiker STAR Labs, montou e publicou em 22 de setembro com o título Fable to Haiku: How a Malicious Repo Tricked Claude Code Into Running Malware. O resumo de outubro da pesquisa em segurança de agentes de código da Adversa o lista entre as técnicas de ataque do mês. O repositório não explora nenhum bug. Cada passo usa um recurso documentado do jeito que seus criadores pretendiam.

Como o repositório revisou a si mesmo

O repositório, northstar-metrics, parece um projeto open source comum, e seu primeiro truque é banal: um CONTRIBUTING.md. Ao pedir um PR, você amarrou o agente ao fluxo de contribuição do projeto, e um agente que segue instruções lê o guia de contribuição. Este diz que os revisores devem rodar o agente de primeira revisão do projeto antes de rodar os testes.

Esse agente mora no repositório, em .claude/agents/first-pass.md. O Claude Code permite que um projeto defina seus próprios subagentes nessa pasta, com um modelo e um escopo no frontmatter. Este define model: haiku e limita a revisão a src/ e docs/. Suas instruções acrescentam que uma leitura na sessão principal não substitui rodar o agente, para que o agente principal não o pule.

Esta é a cadeia completa:

  1. O Fable, o modelo principal, passa a revisão para o subagente do repositório.
  2. O subagente roda no Haiku 4.5 e só olha src/ e docs/.
  3. Ele volta com dois achados que o autor plantou para ele encontrar: uma string de versão fixa no código e uma divergência entre a documentação e o código. Os dois são inofensivos.
  4. O fluxo documentado considera a revisão concluída. Ninguém abre tests/test_compat.py.
  5. O Fable roda pytest.
  6. Antes da sua asserção, test_compat.py decodifica uma URL em hexadecimal, baixa um arquivo Python e o inicia com subprocess. O bloco está envolto em try/except: pass, e a asserção sempre é verdadeira.
  7. Um implante Sliver abre uma sessão de comando e controle. O pytest imprime 11 passed.

O resumo de Cumi sobre por que funciona: o repositório se apoia em três suposições comuns. Projetos podem definir seus próprios subagentes, agentes seguem o fluxo documentado de um projeto, e uma execução de testes verde conta como verificação do código. “Um repo malicioso que entende as três suposições não precisa quebrar nenhuma delas. Só precisa ficar na costura onde elas se encontram.”

A Straiker relatou isso à Anthropic pelo HackerOne em 14 de setembro. A Anthropic fechou como Informative: respeitar a configuração do repositório faz parte da decisão de confiança no workspace tomada pelo usuário.

A decisão de confiança, na prática

A resposta da Anthropic é coerente com o funcionamento do Claude Code. Quando você abre uma pasta, ele pergunta se você confia nela, e você disse sim. Essa decisão cobre o CLAUDE.md, os hooks, os subagentes e a suíte de testes.

O problema é sobre o que você estava decidindo. Você confiou no repositório o bastante para olhar para ele. Era para isso que você o clonou. Em um notebook, confiar nesta pasta o bastante para rodar seus testes significa confiar a ela tudo o que sua conta alcança, porque é ali que o pytest roda. A Straiker lista o que o implante tinha: todo arquivo que você pode ler, credenciais em memória, chaves SSH no disco e todo repositório em que você pode fazer push.

Nenhum revisor, humano ou modelo, lê todos os arquivos de teste de todos os projetos que avalia. Aqui a revisão foi encurtada de propósito, mas muitas revisões honestas também são curtas. O que você pode escolher é onde a suíte de testes roda.

No notebook: o repo escreve a revisão, os testes rodam como vocênorthstar-metrics (o repo)CONTRIBUTING.md“rode o agente first-pass”.claude/agents/first-pass.mdmodel: haikuscope: src/ docs/tests/test_compat.pyfora do escopo da revisãosem jailbreak, sem CVE:só recursos documentadosClaude Code1. Fable delega a revisão2. Haiku lê src/ e docs/3. dois achados plantados, inócuos4. revisão marcada como feita5. $ pytest===== 11 passed =====Seu notebook, como você6. baixa o payload e o executa7. sessão Sliver abertaO implante alcançatodo arquivo que você lê~/.ssh/id_ed25519~/.aws .env tokenspush em todo repo
A cadeia da Straiker em um notebook. O repositório desvia a revisão para um modelo mais barato que nunca vê os testes, o pytest executa o payload como o desenvolvedor, e o implante herda tudo o que o desenvolvedor alcança. A única coisa que o usuário vê é a última linha.

O mesmo repositório em uma máquina Bromure

No Bromure Agentic Coding, o Claude Code roda dentro de uma VM Linux no seu Mac. Cada máquina tem seu próprio disco, seu próprio diretório home e suas próprias credenciais, e todo o seu tráfego sai por um proxy no Mac. A decisão de confiança no workspace continua sendo sua. O que muda é o que o repositório confiável consegue alcançar.

Reproduza a cadeia da Straiker dentro de uma máquina Bromure. Os passos um a cinco acontecem como antes: o agente lê o guia de contribuição, o subagente revisa as pastas erradas e o pytest roda. Do passo seis em diante, o repositório esbarra no Mac.

O download passa pelo Mac

O payload precisa ser buscado. O teste decodifica uma URL e pede um arquivo a um host de que o projeto nunca precisou. Dentro de uma máquina Bromure só existe um caminho para fora: o switch de rede virtual desvia toda conexão nas portas 80 e 443 para o proxy no Mac e confere qualquer outra conexão, seja qual for o protocolo, com as regras da máquina.

Essas regras ficam no painel Guardrails da máquina, em Outbound connections: entradas de permitir e negar por host, faixa de IP, protocolo e porta, com uma configuração Unmatched traffic em Allow ou Deny. Uma máquina que você usa para avaliar código de estranhos precisa de uma lista curta: o provedor do modelo, os registros de pacotes, o GitHub.

allow web api.anthropic.com
allow web pypi.org
allow web files.pythonhosted.org
allow web github.com
default deny

Com essas regras, o download em test_compat.py nunca conecta, e o try/except: pass que o autor colocou para esconder falhas agora esconde esta. O pytest continua imprimindo 11 passed. A diferença está no Mac. A Security Timeline da máquina tem uma linha vermelha Firewall com um veredito de negação, o host que o teste tentou alcançar e a porta, com horário segundos depois de o agente rodar pytest. A suíte de testes de um cliente de métricas não tem motivo nenhum para contatar um host de que você nunca ouviu falar, e agora você sabe que ela tentou.

O tráfego posterior do implante recebe o mesmo tratamento. Seja o Sliver ligando para casa por HTTPS, por TLS mútuo na própria porta, por WireGuard ou por DNS, a conexão sai pelo switch, e o switch confere cada protocolo com a mesma tabela.

O implante encontra substitutos

Suponha que a máquina permitia tráfego não correspondente, o payload chegou e o Sliver abriu sua sessão. O atacante agora tem um shell na VM, com o mesmo alcance que a Straiker listou para o notebook. Em uma máquina Bromure esse alcance cobre menos.

  • Arquivos. A VM contém seu próprio diretório home e as pastas de projeto que você escolheu compartilhar com aquela máquina. O diretório home do seu Mac, seus outros projetos, seus perfis de navegador e suas Chaves não estão lá dentro.
  • Credenciais. O Bromure mantém as chaves de API reais, tokens OAuth, tokens do git, segredos de nuvem e senhas de banco de dados criptografados no Mac. A VM tem substitutos no formato certo: ANTHROPIC_API_KEY=sk-ant-api03-brm-…, um token ghp_ que o GitHub recusaria, uma chave secreta da AWS que não assina nada. O proxy coloca o valor real depois que a requisição sai da VM, e só para o host a que a credencial pertence.
  • Chaves SSH. ls ~/.ssh/id_* não retorna nada. O SSH_AUTH_SOCK da VM é uma ponte para um ssh-agent no Mac. A VM pode pedir uma assinatura, mas não pode ler a chave. Ative Ask before use para uma chave, e cada pedido de assinatura abre um diálogo no Mac.

Um implante que junta o que consegue e manda para casa tropeça num segundo fio. Um token substituto tem um único destino legítimo, o host para o qual foi emitido. Quando o proxy vê sk-ant-api03-brm-… ou aquele token ghp_ indo para qualquer outro lugar, ele recusa a requisição com HTTP 451, pausa a VM e abre no Mac um alerta intitulado This environment may have been compromised, que nomeia o token, o host para o qual foi emitido e o host para o qual foi enviado. Só saiu um substituto, então não há nada para rotacionar.

O push pergunta primeiro

O último item da Straiker, os repositórios com acesso de push, é o que transforma um notebook comprometido em um incidente de cadeia de suprimentos. Um atacante com um shell e suas credenciais do git pode fazer push de um commit em todo projeto que você mantém.

Em uma máquina Bromure, um push para GitHub, GitLab ou Bitbucket é uma escrita, e as máquinas novas vêm com Prompt before write para cada serviço protegido. O proxy segura o push e o macOS mostra Allow write on "github.com" from workspace "…"? com a operação como ela apareceu na rede, POST /acme/app.git/git-receive-pack. Você não pediu push nenhum, então clica em Don't allow, e o implante recebe um 403 terminando em blocked by Bromure Guardrails. O macOS desenha o diálogo a partir do que o proxy leu, então nada dentro da VM consegue mudar o texto.

O trace mostra a revisão que não aconteceu

Quando a poeira baixa, você quer saber o que o agente fez. No nível de tracing padrão, AI request details, o proxy guarda cada troca com o provedor do modelo, criptografada no Mac, e o Trace Inspector desmonta cada uma em chamadas de ferramenta, comandos de shell e leituras de arquivo.

Para este repositório, o registro responde às perguntas que importam:

  • Qual modelo fez a revisão? O corpo da requisição capturada o nomeia: claude-haiku-4-5, um turno depois de a sessão principal ler .claude/agents/first-pass.md.
  • O que ele leu? Cada chamada Read, com seu caminho. Há chamadas para arquivos em src/ e docs/, e nenhuma para tests/test_compat.py.
  • O que rodou? A chamada Bash que iniciou o pytest e, ao lado dela, na Security Timeline, a linha de firewall do download que ela causou.

O repositório controla o que o agente vê e o que o pytest imprime, mas não tem como editar o trace. O proxy escreve esse registro no Mac, a partir do próprio tráfego.

No Bromure: os testes rodam em uma VM e as saídas ficam no MacVM da máquina: “eval”~/northstar-metrics$ pytest → test_compat.pyO que um implante encontrariaANTHROPIC_API_KEY=sk-ant-api03-brm-…GH_TOKEN=ghp_… (substituto)~/.ssh: sem chaves privadassó o home desta máquinanem home do Mac, nem outros projetostoda conexão sai pelo switchSwitch + proxy, no MacFirewall: host do payload → denylinha vermelha na Security TimelineSubstituto fora do escopo → 451, VM pausadaTrace: modelo claude-haiku-4-5,Read src/… docs/…, Bash pytestDiálogo do macOS: Allow write on …?POST /acme/app.git/git-receive-packchaves reais só para o próprio hostInternetapi.anthropic.compypi.orggithub.comhost do payload:nunca contatadoGuardado no Mac, fora da VMChaves de API e tokens OAuth · credenciais de git, nuvem e BD · chaves SSH privadas · traces criptografados · Security Timeline
O mesmo repositório em uma máquina Bromure. A revisão continua indo para o Haiku e o pytest continua rodando, mas a VM só tem credenciais substitutas e nenhuma chave privada, o download do payload para no firewall de saída do Mac, os pushes esperam um diálogo do macOS, e o trace registra qual modelo revisou o quê.

Uma máquina para código que você não leu

O relatório da Straiker termina com três recomendações: exigir aprovação antes de rodar um subagente definido pelo repositório, ler os arquivos de teste antes de rodá-los e tratar arquivos como .claude/agents/, hooks e definições de workflow como entrada não confiável. As três são sensatas, e as três dependem de alguém se lembrar, toda vez, numa tarefa que parecia uma revisão de rotina.

No Bromure o hábito seguro custa alguns cliques. Uma máquina nova é um clone copy-on-write de uma imagem Ubuntu assinada, então quase não ocupa disco. Isso sugere uma configuração para avaliar código de fora:

  1. Crie uma máquina chamada eval. Dê a ela um provedor de modelo e mais nada: nenhum token do git com permissão de push, nenhuma chave de nuvem, nenhum banco de dados.
  2. Configure a saída dela para negar por padrão. Em Guardrails › Outbound connections, permita o provedor do modelo, os registros de pacotes e o GitHub, e coloque Unmatched traffic em Deny.
  3. Clone dentro da VM, não numa pasta compartilhada. Comece a sessão numa pasta nova no home da máquina. Uma pasta compartilhada é uma janela ao vivo para o seu Mac, e o home da própria máquina é a parte que o Bromure consegue rebobinar ou apagar.
  4. Ou torne-a descartável. bromure-cli vm run --rm inicia uma máquina que é apagada, disco e tudo, quando para.
  5. Leia a Timeline antes de adotar a dependência. Uma linha Firewall de negação numa execução de testes é um achado que nenhum revisor relatou, não importa o que o pytest imprimiu.

Se algo passar, você limpa uma máquina só. Rewind home… volta o diretório home para o checkpoint de um boot anterior. Uma máquina que o detector de comprometimento marcou se recusa a iniciar até você escolher Wipe and Launch, que reconstrói o disco e o home dela e mantém suas configurações e as credenciais reais no Mac. A conta do seu notebook, seus outros projetos e suas outras máquinas nunca estiveram lá dentro.

Quando a biblioteca tiver provado seu valor, leve-a para a máquina que faz o trabalho de verdade. Essa máquina também só tem chaves substitutas, e os pushes dela também param no diálogo do Mac.

O download precisa pedir

Toda conexão que a VM abre passa pelo switch virtual e pelo proxy no Mac. Com o tráfego não correspondente negado, um teste que busca um payload de um host novo falha, e a Security Timeline mostra a tentativa.

O shell encontra substitutos

As chaves de API reais, os tokens do git e os segredos de nuvem ficam criptografados no Mac, e as chaves SSH privadas nunca entram na VM. Um substituto enviado para qualquer lugar que não seja seu host retorna 451 e pausa a máquina.

Um push precisa de um clique no Mac

Com Prompt before write, o padrão nas máquinas novas, um git push espera um diálogo do macOS que mostra a operação exata. Um implante não consegue respondê-lo.

Um registro que o repo não edita

O trace no Mac mostra qual modelo revisou o código, quais arquivos ele leu e qual comando rodou em seguida, não importa o que o terminal imprimiu.

Confie menos no repositório

O repositório não fez nada exótico. Escreveu um guia de contribuição, definiu um agente auxiliar e entregou um arquivo de teste. Os projetos vão continuar fazendo as três coisas, porque são úteis, e os agentes vão continuar seguindo-as, porque seguir as convenções de um projeto é o que faz um agente ser bom no que faz.

A resposta da Anthropic deixa a decisão com você, e é aí que ela deve ficar. Tome-a numa máquina feita para isso: uma em que confiar num repositório o bastante para rodar seus testes significa confiar a ele uma VM, alguns substitutos e uma lista curta de hosts. Depois deixe a suíte de testes imprimir o que quiser.