O sandbox que segurava a chave
Em 18 de maio de 2026, a Lasso Security divulgou dois ataques contra o NemoClaw da Nvidia — o sandbox que executa o agente de codificação autônomo OpenClaw. O sandbox funcionou exatamente como a Nvidia disse que funcionaria. O agente dentro do sandbox ainda assim empurrou o token GitHub do usuário para um pull request controlado pelo atacante, codificado como emoji para escapar do scanner estático de segredos do GitHub. A pergunta interessante não é se o sandbox está quebrado. É se um sandbox com um arquivo de credenciais em texto claro dentro dele algum dia foi um sandbox no sentido arquiteturalmente útil, e o que a resposta implica para todo mundo embarcando um agente de codificação em 2026.
Duas coisas podem ser simultaneamente verdadeiras. A primeira é que
o sandbox NemoClaw da Nvidia, que envolve o agente de codificação
autônomo OpenClaw em um cluster K3s dentro de um contêiner Docker
privilegiado, funcionou exatamente da forma que a Nvidia documentou
que funcionaria. A segunda é que, em 18 de maio, a Lasso Security
publicou um writeup no qual um script postinstall malicioso do
npm — rodando dentro desse sandbox, fazendo apenas coisas que o
sandbox estava configurado para permitir — leu o token GitHub do
usuário de um arquivo de configuração em texto claro, codificou-o
como emoji para derrotar o scanner de segredos do GitHub, e
empurrou o resultado para um pull request controlado pelo atacante
através do mesmo binário gh que a política de saída havia
gentilmente colocado na lista branca. A Nvidia respondeu que isso
estava fora do escopo do programa de bug bounty, sob o argumento de
que o sandbox se comportou exatamente como configurado para
rodar. Isso é, em um sentido que a maioria das pessoas de segurança
vai achar familiar, tanto correto quanto irrelevante.
Eis a história em três frases. Em 18 de maio de 2026, a Lasso
Security
publicou
uma cadeia de ataque contra o NemoClaw, o sandbox da Nvidia para
rodar o OpenClaw — um agente de codificação autônomo que, como o
Claude Code ou o agente do Cursor ou o Codex CLI, tem permissão
para executar npm install, gh pr create, e git push em nome
do usuário. A cadeia tem duas metades: uma metade de exfiltração
de credenciais, na qual um pacote malicioso leu
/sandbox/.openclaw/openclaw.json (um arquivo em texto claro
contendo o token GitHub do usuário junto com chaves para serviços
da Anthropic, OpenAI, Gemini, e Nvidia), reescreveu o token como
uma sequência de emojis usando uma tabela de lookup construída em
tempo de execução, e empurrou o payload codificado para um PR
público através do binário gh da lista branca; e uma metade de
persistência, na qual uma injeção de prompt plantada em um arquivo
fonte rotineiro induziu o agente a reescrever seu próprio
SOUL.md — o arquivo de memória comportamental do OpenClaw — com
um backdoor que sobrevive a todas as sessões subsequentes.
A BDTechTalks
cobriu a divulgação no mesmo dia; a resposta da Nvidia, citada em
ambos os writeups, foi que o sandbox se comportou exatamente como
configurado para rodar e que os cenários caem fora do escopo do
programa.
A resposta da Nvidia é, em um sentido técnico restrito, verdadeira.
O sandbox é um sandbox. O pod K3s é um pod K3s. A política de
saída, que permite ao gh falar com github.com, é uma política
que permite ao gh falar com github.com. Nada escapou de seus
limites de capacidade. Nada escalou. O contêiner não vazou. Da
perspectiva do runtime, cada byte que saiu fez isso através de um
binário que o usuário concordou que poderia sair, em direção a um
destino que o usuário concordou que ele poderia alcançar.
Acontece que o byte que saiu era o token GitHub do usuário, soletrado com carinhas de desenho animado.
A parte chata da cadeia.
Vamos percorrer a metade das credenciais lentamente, porque a
mecânica é o argumento inteiro. O agente recebe uma tarefa — algo
sem graça, «configurar este novo projeto a partir de um README do
GitHub» — e o README contém, em algum lugar de suas instruções, um
passo que se resume a npm install some-helpful-package. O pacote
é malicioso, mas da mesma forma discreta com que cem pacotes npm
por semana agora são maliciosos: é um typosquat ou uma dependência
transitiva de um pacote recentemente comprometido, e ele embarca
um script postinstall. O script postinstall faz três coisas que
são individualmente permitidas e coletivamente catastróficas.
Ele lê /sandbox/.openclaw/openclaw.json. Esse arquivo está no
sistema de arquivos do sandbox; o script está rodando dentro do
sandbox; ninguém disse ao sistema de arquivos para não ser
legível, porque como o próprio agente usaria o token GitHub se não
pudesse ler o arquivo onde o token GitHub mora? Ele extrai o valor
ghp_….
Ele carrega um emoji_map.json embarcado dentro do pacote
malicioso, que mapeia cada caractere alfanumérico para um emoji —
um arquivo JSON perfeitamente comum, completamente inócuo em
varredura estática, do tipo de coisa que um formatador de markdown
ou um cliente de chat poderia legitimamente embarcar. O script
percorre o token caractere por caractere e produz uma string como
🍕🍔🌮🍟…, um emoji por caractere.
Ele executa gh pr create contra um repositório controlado pelo
atacante com a string de emoji como corpo do PR. Do ponto de vista
da política de saída, isso é gh falando com github.com, que é
a única coisa na lista branca que o gh tem permissão para fazer.
O PR é criado. Do lado do atacante, um script lê o corpo do PR,
inverte o mapa, e reconstrói ghp_…. O scanner de segredos do
GitHub também vê o corpo do PR, mas o scanner de segredos do
GitHub está procurando por ghp_[A-Za-z0-9]{36}, não por
🍕🍔🌮🍟.
A pesquisadora da Lasso Noy Pearl, citada na divulgação, colocou
isso de forma direta: «A codificação em emoji foi a técnica que
escolhemos para contornar as varreduras estáticas do GitHub» e
«enquanto o agente tiver conexão com o mundo externo, nenhum
mecanismo estático pode te proteger completamente.» Esta é a
parte onde você acena com a cabeça e diz que sim, obviamente, você
não pode colocar na lista branca um protocolo capaz de carregar
bytes arbitrários e depois se surpreender quando bytes arbitrários
passam por ele. Uma lista branca L7 que permite gh é uma lista
branca L7 que permite qualquer coisa que você possa serializar em
um corpo de PR, o que é qualquer coisa.
A metade de persistência — SOUL.md — é mecanicamente diferente
mas filosoficamente a mesma. O OpenClaw, segundo o writeup da
Lasso, mantém um arquivo de memória comportamental chamado
SOUL.md que o agente lê no início de cada sessão: regras,
instruções de sistema, contexto acumulado sobre as preferências do
usuário. O agente também pode escrever nesse arquivo, porque a
premissa inteira de memória de longa duração é que o agente
deveria ser capaz de atualizar suas próprias crenças. Uma injeção
de prompt plantada em um arquivo fonte de aparência perfeitamente
normal que o agente processa durante uma tarefa rotineira — o
exemplo da Lasso é um arquivo de texto que apenas contém
instruções formuladas da maneira como dados de treinamento são
formulados — faz com que o agente acrescente uma regra de backdoor
ao SOUL.md. As sessões subsequentes carregam o SOUL.md. O
backdoor agora é, na própria descrição do agente sobre si mesmo,
as preferências do agente. O enquadramento de Pearl sobre a defesa
«comportou-se como configurado» da Nvidia é o afiado: «O sandbox
se comportou como configurado é um argumento aceitável quando a
coisa rodando dentro é um programa determinístico. Ele não
sobrevive ao contato com agentes guiados por LLM, cujo
comportamento é moldado em tempo de execução por cada pedaço de
texto que eles ingerem.»
Um token em um arquivo é um token em um arquivo.
A frase arquitetural que quero escrever aqui, antes de chegar ao que o Bromure faz a respeito de tudo isso, é esta: um segredo de longa duração que vive como um arquivo em texto claro dentro do mesmo raio de explosão que o código que o agente executa é, para fins de qualquer adversário capaz de executar código nesse raio de explosão, equivalente a um segredo público. O sandbox não muda isso. O pod K3s não muda isso. A lista branca de saída não muda isso, porque a lista branca de saída tem permissão para falar com o GitHub, e falar com o GitHub — por design, por intenção do usuário, pela razão inteira pela qual o agente existe — é o mesmo canal que o pacote malicioso vai usar.
Existe uma correção bem-compreendida para isso, e ela antecede os agentes de IA em décadas, e a indústria de segurança vem usando-a discretamente desde os anos 1990. A correção é colocar o segredo do outro lado de uma fronteira de processo e intermediar seu uso, nunca seu valor.
O exemplo canônico é o ssh-agent. Sua chave privada SSH vive em
uma região de memória pertencente ao processo ssh-agent. Quando
o ssh precisa se autenticar, ele não diz «me dê a chave»; ele
envia os bytes do desafio por um socket de domínio Unix e recebe
de volta uma assinatura. A chave nunca atravessa o socket. Um
binário malicioso rodando como o mesmo usuário pode pedir ao
ssh-agent para assinar coisas, certo — esse é o ponto inteiro do
agente — mas ele não pode ler a chave, não pode copiá-la, não pode
exfiltrá-la, não pode enviá-la para casa. Quando a sessão termina,
a chave morre com o processo. WebAuthn faz estruturalmente a mesma
coisa para navegadores: a chave privada vive no TPM ou no Secure
Enclave, a página pede ao navegador para assinar um desafio, a
página nunca vê a chave. A migração de uma década da indústria
para longe de senhas-em-localStorage é, quando você olha de perto,
a mesma migração que o NemoClaw precisa fazer. Apenas um andar
acima.
E funciona para o GitHub também. O CLI gh vem com um helper de
credenciais que, no macOS, pode armazenar o token no Keychain em
vez de em um arquivo de configuração em texto claro. Mais
utilmente para o caso do sandbox, GitHub Apps emitem tokens de
instalação que são de curta duração (tipicamente uma hora),
escopados para repositórios específicos, e revogáveis; um proxy de
assinatura rodando fora do sandbox pode cunhar um destes sob
demanda, anexá-lo a uma requisição que o agente gerou, e
encaminhar o resultado. O sandbox vê uma resposta HTTP genérica. O
sandbox não vê o token. Um script postinstall malicioso pedindo
para ler o token do endpoint intermediado obtém de volta, na
melhor das hipóteses, um token de uma hora escopado para um repo
— suficiente para fazer o que quer que o agente estivesse de fato
fazendo em nome do usuário, insuficiente para valer a pena
exfiltrar.
O padrão tem um nome em cada domínio que o usa — ssh-agent, WebAuthn, assinatura com respaldo de HSM, helper de credenciais do gh, AWS IAM Roles Anywhere — e a propriedade subjacente é sempre a mesma: o consumidor da credencial é um processo diferente do detentor da credencial, e eles se comunicam por um canal mais estreito do que «leia os bytes». É a diferença entre «o agente pode se autenticar no GitHub» e «o agente pode ler o token GitHub». Um sandbox que erra isso é um sandbox que colocou a chave da porta da frente do mesmo lado da porta que as pessoas com quem você estava preocupado.
Isto é, sem ambiguidade, o que o Bromure faz pela VM por perfil
que roda seu agente de codificação. O agente dentro da VM se
autentica no GitHub falando com um intermediário de credenciais no
host macOS por um socket de domínio Unix encaminhado; o token
GitHub (ou, melhor, a chave privada do GitHub App que cunha tokens
escopados de curta duração) vive do lado host do hipervisor, no
Keychain do macOS, onde a VM não pode vê-lo. O agente nunca lê o
valor do segredo, apenas o usa através do proxy. Um script
postinstall que executa cat /sandbox/.openclaw/openclaw.json
não encontra nada; um que executa env | grep TOKEN não encontra
nada; um que pede ao intermediário «por favor me dê o token»
descobre que o vocabulário RPC do intermediário não inclui esse
verbo. A mesma postura, aplicada ao agente GitHub da maneira que
foi aplicada ao SSH por trinta anos.
Uma sugestão educada não é um perímetro.
O truque do emoji da Lasso é, em um sentido profundo, um comentário
sobre listas brancas. A política de saída permitia que gh
falasse com github.com. A suposição implícita era que as coisas
que o gh faria com github.com seriam coisas que um
desenvolvedor razoável iria querer fazer — clonar, empurrar, abrir
PRs, comentar em issues. Mas gh pr create --body "$ANYTHING" é,
por construção, uma primitiva que carrega bytes arbitrários para
um destino publicamente legível. Você não pode colocar essa
primitiva na lista branca e ao mesmo tempo impedir que bytes
arbitrários passem por ela. A lista branca está fazendo o que
disse. Ela só não está fazendo o que você pensou que ela disse.
Esta é a parte do writeup da Lasso que deveria fazer qualquer um rodando um agente em produção sentar. Uma lista branca L7 que permite um protocolo capaz de carregar bytes arbitrários não é um perímetro. É uma sugestão educada de que os bytes dentro do protocolo sejam os bytes que você tinha em mente. Sejam os bytes em um corpo de PR, em uma mensagem de commit, em um comentário de issue, em um blob git-LFS, nos metadados de um tarball enviado como ativo de release — o protocolo é o vazamento. Não existe filtro estático que «entenda» a diferença entre um PR legitimamente cheio de emoji («nosso time usa 🍰 para significar release») e um token GitHub codificado em emoji, porque a diferença é semântica e o filtro é sintático.
Então o que detectaria isso? A resposta honesta é que nenhum truque sozinho faz; a estrutura que faz é uma combinação, e cada peça é desinteressante por si só.
A primeira peça é orçamentos de bytes de saída por sessão. Uma sessão cujo briefing é «corrigir um typo no README.md» não deveria estar empurrando 14 kilobytes de blob binário para um corpo de PR, nunca, por nenhuma razão. Você não precisa saber o que são os bytes; você precisa saber o que a sessão deveria entregar. Incompatibilidade de forma entre briefing e saída é computável, mas apenas se algo fora do agente estiver mantendo registro do briefing e da saída no mesmo lugar. O auto-relato do agente não serve; o agente é a coisa sob suspeita.
A segunda é detecção de anomalia de forma de conteúdo em operações git de saída. Um PR cujo corpo é 95% emoji, ou 95% base64, ou 95% hex, em um repositório cujo histórico contém zero desses PRs, é uma bandeira. Não é prova; é uma bandeira. O lugar certo para computar essa bandeira é o proxy de saída, que vê a requisição de saída antes que ela deixe o host. O proxy de nível hipervisor do Bromure é o lugar que pode computar isso sem confiar no convidado, porque o convidado é exatamente a coisa sob suspeita.
A terceira é diffs de fim de sessão do que saiu da caixa contra
o que a sessão deveria produzir. No fim de uma sessão, a VM
produziu um conjunto de requisições HTTP de saída, um conjunto de
escritas de arquivo no projeto montado, um conjunto de commits
git. Se o briefing da sessão era «corrigir um typo», e as
requisições de saída incluem
POST /repos/attacker-handle/random-repo/pulls, isso é um diff
que pode ser mostrado a um humano em duas linhas. Não bloqueado,
necessariamente — às vezes agentes genuinamente precisam fazer
coisas surpreendentes — mas mostrado. O padrão atual em 2026,
através da maioria dos produtos de agente de codificação, é
«confia em mim, funcionou», e o usuário não tem artefato para
inspecionar mesmo se quisesse.
O pipeline de auditoria é o artefato.
Esta é a parte onde vale a pena dizer em voz alta o que o Bromure
Enterprise de fato entrega, porque a alternativa — confiar nos
próprios logs do agente — é o que levou o NemoClaw a essa posição
em primeiro lugar. Os logs do agente são escritos pelo agente. Se
o agente foi instruído por um SOUL.md envenenado a mentir sobre
o que fez, os logs do agente vão mentir sobre o que ele fez.
Existe exatamente um lugar que pode produzir um registro verdadeiro
do que um agente fez, e é a camada abaixo do agente.
O Bromure Enterprise registra, do lado host do hipervisor, «a sessão completa — chamadas de ferramentas, comandos shell, edições de arquivo, códigos de saída» como JSON Lines. O tracing está ligado por padrão; engenheiros não fazem opt-in; o agente dentro da VM é Claude Code, Codex, ou Cursor sem modificações. O stream é «capturado fora da VM em um fluxo de JSON Lines resistente a adulteração e entregue ao sink de logs que você já alimenta (SIEM, data lake, arquivo de retenção).» Uma dependência comprometida dentro da VM não pode apagá-lo, porque as escritas acontecem do outro lado da fronteira do hipervisor que a dependência não pode atravessar.
As consequências interessantes:
- «Esta sessão empurrou para um repo que não era o repo do usuário» é um grep. Não um grep metafórico. Um grep literal contra o JSON Lines de ontem.
- «Esta sessão abriu um PR cujo corpo não parece prosa» é uma regex contra o payload de chamada de ferramenta capturado.
- «Algo modificou
SOUL.md» é uma consulta em eventos de edição de arquivo. Se um scriptpostinstallreescreveu o arquivo de identidade do agente, esse fato vive no trace independentemente de o próprio agente ter mencionado ou não. - Replay. Os traces se anexam a um pull request. O revisor lê o diff e a sequência de prompts, chamadas de ferramenta, e comandos shell que produziram o diff. Ou, na linguagem da página de produto do Bromure: «replay do dia em que o modelo decidiu deletar a pasta migrations.» Replay forense é a parte desta história que não existe em nenhum outro lugar, porque requer ter capturado as entradas e as saídas e as respostas do modelo no mesmo stream — o que o próprio agente não pode produzir sem primeiro ser confiado para não mentir.
Nada disso pega um atacante determinado que molda a exfiltração para parecer prosa; não existe perímetro contra um adversário disposto a gastar bytes em disfarce. O que isso pega é todo atacante que não fez isso, que é a maioria deles, e produz o registro forense que transforma o próximo incidente de «não temos ideia do que o agente fez» em «temos a linha 14.332 do trace da sessão de ontem». O padrão de 2026 de «o agente disse que teve sucesso» é equivalente a rodar produção em um servidor sem logs.
E quanto à injeção de prompt dentro da sessão?
Vale a pena ser claro sobre o que o isolamento por VM não corrige, porque as pessoas mais propensas a ler este post são as mesmas pessoas mais propensas a serem informadas, por alguém vendendo o produto oposto, de que VMs são uma panaceia. Não são.
O backdoor do SOUL.md — e qualquer injeção de prompt que tenha efeito durante uma sessão — roda dentro da VM, com qualquer escopo que o usuário concedeu ao agente. Se o usuário disse ao agente «criar um PR neste repo», a injeção de prompt pode criar um PR neste repo. Se o agente tem acesso de escrita à pasta do projeto, a injeção de prompt pode escrever um backdoor na pasta do projeto. A VM não revisa o diff. O que a VM faz é manter o diff e sua proveniência em um lugar onde alguém possa revisá-lo depois, o que acontece de ser a parte que estava faltando.
O que o isolamento por VM mais a intermediação de credenciais corrige é a parte catastrófica — a parte onde um único postinstall ruim sai com um token GitHub permanente, um token npm permanente, suas credenciais AWS, e root no laptop do desenvolvedor. Nenhuma dessas credenciais vive dentro da VM; as credenciais vivem no Keychain do host e só são alcançadas através de um proxy cujo vocabulário RPC não inclui «me dê os bytes». Um token que o agente pode usar através de um proxy é um token que o agente não pode exfiltrar, porque exfiltração requer ter os bytes para enviar.
A capacidade restante do atacante dentro de um equivalente ao
OpenClaw hospedado pelo Bromure — o que ele pode fazer uma vez que
fez prompt injection no agente — é fazer o agente executar, dentro
do escopo autorizado da sessão, algo que o usuário não pretendia.
Abrir um PR com um título estranho. Reescrever o SOUL.md no
projeto. Adicionar um postinstall próprio. Todos estes são
eventos observáveis: cada edição de arquivo atinge o stream de
auditoria JSON Lines, cada requisição de saída atinge o proxy de
saída, cada prompt e chamada de ferramenta é registrado fora do
agente. Não se pede à VM para impedir o agente vítima de prompt
injection de fazer dano dentro do escopo — pede-se a ela que torne
esse dano visível e limitado à sessão, para que o host fique limpo
e o usuário tenha um trace para ler. Persistência dentro da VM
ainda é possível; persistência dentro da VM que ninguém pode ver
não é, o que é uma distinção significativa.
Acoplado com intermediação de credenciais, o pior caso para o ataque em um agente hospedado pelo Bromure se parece com: o agente entrega um PR ruim, no repo para o qual já estava autorizado, usando um token de instalação de curta duração, e cada passo aparece em um log resistente a adulteração no host. Isso não é dano zero. Está muito longe de «o atacante tem meu token GitHub permanente, exfiltrado como emoji, mais um backdoor persistente no arquivo comportamental do agente, mais nenhum registro de que qualquer disso aconteceu».
O que é realmente estrutural aqui.
Se você ler o writeup da Lasso e a resposta da Nvidia em sequência, o desacordo não é realmente sobre se o NemoClaw tem um bug digno de CVE. A Nvidia está certa de que o sandbox fez o que estava configurado para fazer. A Lasso está certa de que o que ele estava configurado para fazer é insuficiente. O desacordo é sobre onde a fronteira de confiança pertence.
A Nvidia, na resposta que Pearl cita, desenha a fronteira em «a
política configurada». Dentro da política, vale tudo; fora da
política, o cliente está por sua conta. Esta é uma posição normal
de responsabilidade compartilhada para um vendedor de
infraestrutura. Não é, contudo, uma defesa contra o modo de falha
que a Lasso demonstrou, porque o modo de falha está dentro da
política. A política permite gh. gh pode carregar o token. A
política é autoconsistente e inadequada ao mesmo tempo.
A alternativa estrutural — a posição que os posts de codificação agêntica neste blog vêm argumentando em linguagem diferente por seis meses — é desenhar a fronteira na credencial e na observação, não no binário e no destino. A intermediação diz que a credencial nunca entra no sandbox. O pipeline de auditoria de nível hipervisor diz que o que quer que o agente faça dentro do sandbox é capturado por algo no qual o agente não pode escrever e não pode desligar. Juntos eles formam um sandbox onde o pior que um postinstall malicioso pode fazer é o pior que o agente estava autorizado a fazer, no repo que ele já estava tocando, com credenciais que ele não pode exfiltrar porque nunca as teve, e com cada digitação de evidência sentada no seu SIEM.
O pessoal da Nvidia não está errado. A arquitetura do OpenClaw é a arquitetura de cada agente de codificação embarcado este ano, e isso inclui agentes de codificação não rodados dentro de um sandbox de jeito nenhum. O que a Lasso encontrou no NemoClaw é, estruturalmente, o que a Wiz e a Snyk e a Socket encontram a cada duas terças-feiras no Cursor e no Windsurf e no modo YOLO do GitHub Copilot. A classe de problema é «credenciais em texto claro de longa duração acessíveis a tudo o que o agente executa, sem registro do que o agente fez com elas», e a classe de correção é «intermediar a credencial, observar a sessão, guardar os recibos».
Uma última coisa.
Há uma versão deste post onde a lição é «não confie em sandboxes». Essa versão está errada. Sandboxes são ótimos. Há uma versão onde a lição é «agentes de IA são perigosos demais para serem embarcados». Essa versão é, na prática, irrelevante — eles estão embarcados, a pergunta é como. A versão que se sustenta é aquela em que o sandbox para de ser solicitado a fazer trabalho que o sandbox não pode, por construção, fazer.
Um sandbox não pode esconder uma credencial de um processo que roda dentro dele. Um sandbox não pode dizer a diferença entre um corpo de PR com emoji e um token codificado em emoji. Um sandbox não pode transformar um protocolo longo na lista branca em um protocolo curto. O que um sandbox pode fazer é manter as credenciais em algum outro lugar, manter o agente em um lugar onde o hipervisor vê cada movimento que ele faz, e escrever esse registro em um sink que o agente não pode alcançar. Essa é a configuração onde o mesmo pacote npm malicioso, no mesmo tipo de script postinstall, encontra um sistema de arquivos vazio, um socket de credenciais cujo único verbo é «assine esta coisa para mim, brevemente», e um hipervisor do outro lado da parede que vem anotando a conversa inteira.
Bromure Agentic Coding é a configuração onde isso é o padrão. É gratuito, open-source, e disponibilizado hoje. O próximo emoji já está sendo enviado.