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

A sandbox abriu a porta

A CVE-2026-65105 é uma falha de rede no NVIDIA NemoClaw, uma ferramenta cujo propósito inteiro é rodar um agente de IA dentro de uma sandbox. A sandbox é um contêiner Docker, contêineres não conseguem alcançar a interface de loopback do host, então o NemoClaw passou a iniciar o servidor de modelo local em todas as interfaces — o que desligou justamente a única verificação que teria impedido uma página web de falar com ele. O que uma página visitada levou foi o chat template do modelo, a camada que renderiza todo prompt de sistema antes de o modelo o ler. No Bromure Agentic Coding o motor local escuta no loopback, numa porta que o convidado nunca fica sabendo, e o convidado o alcança por vsock em vez de pela rede.

Foi a contenção que abriu o buraco. Uma sandbox que não consegue alcançar a interface de loopback da sua máquina empurra o servidor de modelo para todas as interfaces, e a partir daí uma aba do navegador também o alcança.

Você está rodando seu agente de codificação do jeito cuidadoso. Ele vive numa sandbox. O modelo com que ele pensa roda na sua própria máquina, então nenhum prompt sai do prédio. Você fez as duas coisas que todo mundo manda fazer.

Aí você abre uma aba do navegador, e a partir desse momento seu agente começa a escrever código com um defeito dentro que ele deixa de mencionar.

Isso é a CVE-2026-65105, divulgada em 25 de agosto por Elad Luz e Ofek Itach, da Oasis Security, cuja análise chama o caso de sequestro de agente de passagem. Eles levaram o achado ao PSIRT da NVIDIA antes de publicar, e a história saiu no mesmo dia em The Hacker News, SiliconANGLE e CSO Online. A NVIDIA publicou um comunicado e uma correção.

O produto é o NemoClaw, a ferramenta da NVIDIA para implantar um agente OpenClaw dentro de uma sandbox OpenShell com um backend Ollama local para inferência. Contenção é o argumento de venda.

O contêiner não conseguia alcançar o loopback

O Ollama escuta na porta 11434 e, por padrão, liga-se a 127.0.0.1 — a interface de loopback, alcançável apenas a partir da própria máquina.

Sandboxes OpenShell são contêineres Docker. Um contêiner tem seu próprio namespace de rede, então 127.0.0.1 dentro do contêiner é o contêiner, não o host. O agente na sandbox, portanto, não consegue alcançar um servidor de modelo ligado ao loopback do host.

O NemoClaw resolveu isso com uma variável de ambiente. Ele inicia o Ollama com OLLAMA_HOST=0.0.0.0:11434: todas as interfaces da máquina, incluindo a que dá para o wifi do escritório. O instalador continuava imprimindo Using Ollama on localhost:11434 enquanto o socket respondia ao mundo.

Então vem o passo que transforma uma escolha de configuração em vulnerabilidade. A API do Ollama não tem autenticação, então ela carrega duas guardas no lugar. A primeira é uma verificação de CORS no cabeçalho Origin. A segunda é uma lista de permissões no cabeçalho Host que aceita localhost, o nome de host da própria máquina, e nomes terminados em .localhost, .local ou .internal.

A segunda guarda contém uma exceção. O Ollama olha para o endereço em que está ligado e, se esse endereço não for loopback, ele pula a verificação de Host por completo. Dá para ver por que alguém escreveu isso: um operador que liga uma interface pública quer clientes remotos, e uma lista de Host feita para loopback mandaria todos eles embora. O resultado é que a configuração que expõe a porta é a mesma configuração que desarma a verificação que a protege.

Sobra a verificação de CORS, e o DNS rebinding atravessa CORS por construção. O atacante serve a página a partir de um domínio que controla, na porta 11434, e depois faz esse domínio resolver para 127.0.0.1. Origin e Host agora são o mesmo nome escolhido pelo atacante, que o Ollama lê como mesma origem. Cobrimos o mecanismo de rebinding em detalhe na semana passada, então tome como dado: uma página que você visita vira cliente de um serviço na sua máquina, e isso leva cerca de um minuto.

O rebinding é o caminho de entrada interessante. É também o mais difícil. Um servidor em 0.0.0.0 responde a todos os outros dispositivos do segmento de rede sem truque nenhum de navegador: o notebook do visitante, a smart TV, a máquina comprometida duas mesas adiante.

a restriçãosandbox OpenShell = contêiner Dockernamespace de rede próprio, portanto127.0.0.1 = o contêiner, não o hosto agente não alcança o servidor de modeloum problema real, que pedia resposta reala resposta que o NemoClaw entregouOLLAMA_HOST=0.0.0.0:11434todas as interfaces, sem autenticaçãoo instalador imprime: “Using Ollama on localhost”as duas guardas do Ollama1 · verificação de CORS no cabeçalho Origin2 · lista de Host: localhost, nome de host,.localhost .local .internala guarda 2 é pulada quando a ligação não é loopbackdois caminhos de entradauma página: religar o domínio a 127.0.0.1,Origin e Host coincidem e o CORS passaa LAN: discar a porta direto, sem truqueos dois chegam sem autenticação à API inteiraA configuração que expôs a porta é a mesma configuração que desligou a verificação que a protegia.
Por que a porta mudou de lugar. A sandbox é um contêiner Docker, então não alcança a interface de loopback do host, e o NemoClaw resolveu isso ligando o servidor de modelo a todas as interfaces. O Ollama pula a verificação do cabeçalho Host sempre que o endereço de ligação não é loopback, o que deixa apenas uma verificação de CORS — e o DNS rebinding faz de Origin e Host o mesmo nome escolhido pelo atacante.

Eles não queriam execução de código

Alcançar a API é a metade chata. O que a Oasis fez depois de chegar lá é a parte nova.

O movimento óbvio é gravar um prompt de sistema malicioso dentro do modelo. Isso falha aqui: o OpenClaw manda o próprio prompt de sistema no array messages a cada requisição, e o prompt do cliente vence o que está guardado no modelo. Então os pesquisadores desceram uma camada.

O endpoint /api/create do Ollama aceita um campo template. Um chat template é um template Go que transforma o array estruturado messages — papéis, conteúdos, definições de ferramentas — no único bloco corrido de texto que o modelo de fato lê. Ele roda no momento da inferência, depois de o cliente ter entregue suas mensagens, e toca em todas elas.

O ataque são três requisições. Ler o template real com /api/show. Manter a renderização das ferramentas e os tokens especiais exatamente como estão, e mudar apenas como as mensagens de sistema são renderizadas, anexando uma instrução da sua escolha. Gravar de volta com /api/create.

O cliente continua enviando seu próprio prompt de sistema caprichado. O template embrulha esse prompt no do atacante, toda vez, antes que o modelo veja um token dele. Nas palavras de Elad Luz, citadas pelo CSO Online, a instrução fica “uma camada abaixo de qualquer coisa que um guardrail ou um operador consiga ver”.

A edição pousa na definição do modelo em disco, então sobrevive à conversa, ao reset e ao reinício. Os metadados do modelo, seu tamanho e as capacidades listadas continuam iguais, e nada parece diferente numa lista de modelos instalados. Todo consumidor daquele modelo recebe a instrução: a CLI que você cutuca à mão e o agente rodando enquanto você almoça.

A Oasis lista para que serve uma instrução dessas. Nada disso é dramático o bastante para ser notado.

Escrever o bug dentro

Produzir código com um defeito sutil — daqueles que passam bem numa revisão, porque foram escritos para passar bem numa revisão.

Ficar calado

Nunca apontar uma preocupação de segurança. Um agente que avisava de algo e parou de avisar é difícil de perceber.

Guiar a escolha

Recomendar um pacote específico, uma URL específica, uma configuração específica. Você pediu uma sugestão; recebeu uma.

Mandar tudo para casa

Onde o agente tem acesso de rede, despachar o conteúdo das conversas e dos arquivos para um endpoint escolhido pelo atacante.

O resto da API sem autenticação é elenco de apoio: /api/generate e /api/chat para rodar inferência na GPU dos outros, /api/pull para encher um disco, /api/delete para destruir os modelos, /api/push para publicar sob a conta ollama.com da vítima, e /api/me para descobrir o nome de host, a chave pública e o usuário logado de quem você acabou de alcançar.

A sandbox nunca foi o raio de alcance

A Oasis é cuidadosa sobre o que a sandbox fez e não fez. A sandbox OpenShell funciona: ela isola o sistema de arquivos, a rede e os processos, e mantém o endpoint limpo. Um agente dentro de uma organização justifica seu lugar segurando controle de versão, CI/CD, APIs internas, contas de nuvem, chat e uma pilha de servidores MCP. Tome o agente e você comanda esse acesso. O resumo da própria análise é que o verdadeiro raio de alcance segue os recursos que o agente está autorizado a alcançar, não a fronteira da sua sandbox.

Que é a pergunta que um contêiner nunca responde por você, e aqui ela vem com uma reviravolta. A camada de contenção foi o que criou a exposição. O namespace de rede do Docker fez seu trabalho. Contorná-lo colocou o servidor de modelo no wifi.

Onde o modelo local mora num profile

O Bromure Agentic Coding também roda inferência local. Os agentes de um profile podem ser apontados para um modelo no dispositivo em vez de um provedor de nuvem, que é exatamente o recurso que o NemoClaw oferece. O encanamento por baixo é construído ao contrário, e cada passo dele é uma resposta direta a algo lá de cima.

O motor escuta no loopback, e nunca em 0.0.0.0. O servidor de modelo no dispositivo roda no Mac, em 127.0.0.1, numa porta que o kernel entrega no lançamento em vez de um número fixo. A porta dinâmica existe por um motivo sem graça: para que o Bromure nunca brigue com o que já ocupa a tradicional 11434 naquele Mac, Ollama e LM Studio inclusos. Também significa que não existe porta conhecida para uma página mirar.

O convidado não o alcança por uma rede. Dentro da VM, o agente disca 127.0.0.1:11434, o endereço que ele espera. Essa conexão não chega perto de uma rota. Uma ponte na VM emenda o fluxo TCP no vsock, o transporte de socket do virtio, na porta 8446, e a ponta do canal no host o entrega ao motor em loopback. Um canal vsock é um cano entre um hipervisor e um convidado. Ele não tem endereço IP, nome de host nem registro DNS, então não há o que religar, não há o que escanear, e não há como um navegador discar. O convidado nunca fica sabendo em que porta o motor está.

Nada na sua LAN alcança a VM. Em Resources → Network, o modo padrão é NAT: a saída funciona, e nada na sua rede local alcança a VM. Isso fecha a metade do achado do NemoClaw que não precisa de rebinding nenhum, o dispositivo no mesmo wifi discando a porta direto, e fecha sem que você mude uma configuração.

Não há definição de modelo dentro da VM para reescrever. Agentes convidados são fixados a um único identificador de modelo, bromure-local, e o endereçam em bromure.llm, um nome de host sintético sem registro DNS por trás. O proxy dentro da VM intercepta esse nome e o encaminha ao host. Qual modelo responde é um mapeamento que você mantém no host, no painel Local Models do profile, e mudá-lo é um remapeamento do lado do host: nenhuma reconfiguração dentro da VM, nenhum reinício de agente. O template, os pesos e o roteamento vivem todos no Mac. Nada no convidado tem um equivalente a /api/create, porque nada no convidado tem o modelo.

A inferência local passa pelo mesmo pipeline que a de nuvem. Como esse tráfego atravessa o proxy do Bromure em vez de ir direto a uma porta, ele recebe o mesmo tratamento de uma chamada à Anthropic ou à OpenAI: o mesmo trace, o mesmo log e a mesma detecção de prompt injection. O painel Prompt Injection pontua o conteúdo de arquivos, páginas web e saídas de ferramentas que o agente lê, com um modelo PromptGuard local, e pontua os arquivos CLAUDE.md, AGENTS.md e GROK.md que ele carrega como autoridade, com um classificador ModernBERT ajustado mais um scanner determinístico para payloads em Unicode invisível. As detecções vão para o Security Log, ou pausam a requisição, ou devolvem um 451 seco, conforme qual das três respostas você escolheu.

Uma porta em todas as interfacesonde o motor escuta?0.0.0.0:11434 · fixa, bem conhecidacomo a sandbox a alcança?por IP, como todo mundo no wifiquem mais a alcança?uma página que religa, e a LAN, sem autenticaçãoquem pode reescrever o chat template?qualquer coisa que alcance a porta, viaPOST /api/createUm canal sem endereçoonde o motor escuta?127.0.0.1:<porta do kernel> · só o hostcomo a VM o alcança?vsock 8446 — um cano, não uma redequem mais o alcança?ninguém: sem IP, sem nome de host, sem DNSquem escolhe o modelo?você, no painel Local Models do profile;o convidado só conhece o nome bromure-local
Duas maneiras de dar um modelo local a um agente. À esquerda, uma porta HTTP em todas as interfaces, sem autenticação, com uma definição de modelo gravável atrás — alcançável por qualquer dispositivo da rede e por qualquer página que religue um nome de host. À direita, um motor em loopback numa porta atribuída pelo kernel, alcançado a partir do convidado por um canal vsock que não tem endereço para discar, com a escolha do modelo mantida no host.

Agora suponha que o modelo está mentindo para você

Leve um argumento de arquitetura longe o bastante e alguém deveria testá-lo. Então entregue a vitória ao atacante pelo caminho que preferir: um template envenenado, um dia ruim num provedor de modelos, um prompt injection que passou. Seu agente agora trabalha para outra pessoa e a saída dele continua boa de ler. Passe aqueles quatro payloads por um profile.

Guiar a escolha, e a escolha ainda ser julgada. Esse payload é o que mais se apoia na autoridade do modelo, e é o que bate mais na parede. O Bromure avalia cada busca de pacote no proxy do host antes de o agente ver a resposta, em npm, PyPI, Cargo, RubyGems, Maven, NuGet, Go modules e Packagist, e o proxy não pergunta quem sugeriu o pacote. Um portão de idade de dois dias vem ligado por padrão, o que dá conta da maior parte do que um typosquat recém-publicado precisa. Ligue a consulta OSV, aponte o filtro de pacotes para socket.dev ou Delpi, e tire os scripts de instalação, e aí uma recomendação tem de sobreviver a quatro opiniões nas quais o modelo não tem voz. Os arquivos .npmrc e pip.conf dentro da VM podem apertar essas configurações. Não podem afrouxá-las.

Mandar tudo para casa, por um fio que você controla. Exfiltração precisa de saída. Guardrails → Outbound connections é uma tabela de regras no estilo pf — uma ação, um protocolo (tcp, udp, web, any), um host ou CIDR, uma lista de portas, e para web uma lista de métodos HTTP — avaliada de cima para baixo, a primeira correspondência vence, com Unmatched traffic em Allow ou Deny. Ponha em Deny, liste os hosts de que seu trabalho precisa, e o endpoint do atacante não está entre eles. A aplicação acontece no switch virtual e na camada SNI do host, então nada que o convidado faça com o próprio roteamento muda o veredito.

Escrever o bug dentro, e então tentar publicá-lo. Guardrails também carrega um modo para cada serviço que o agente alcança: Block destructive ou Read-only em GitHub, GitLab, Bitbucket, AWS, Kubernetes, DigitalOcean, registries de contêiner e cada endpoint de banco configurado. Em modo somente leitura um git push é classificado como escrita e bloqueado; um fetch é sempre permitido. O commit com backdoor fica na VM.

E o acesso que ele herda é falso. O ponto da Oasis sobre raio de alcance é aquele em que um profile gasta mais esforço. As credenciais na VM são iscas: marcadores brm_… como variáveis de ambiente, um ~/.kube/config sintético com certificados de cliente descartáveis, um blob base64 falso em ~/.docker/config.json, e requisições AWS reassinadas no host, de modo que qualquer coisa que contorne o proxy recebe InvalidSignatureException em vez de acesso. Chaves SSH privadas nunca estão na VM — o host assina através de um agente por profile. Ligue Require approval to use para uma dada credencial e cada substituição vira um diálogo no host com uma concessão limitada no tempo: cinco minutos, uma hora, o resto da sessão. O compartilhamento de pastas é limitado a oito diretórios que você escolhe a dedo, então o resto do seu Mac não está lá para ser lido.

E você enxerga tudo isso. A janela Security Log (Window → Security Log…) é uma única tabela cronológica no host: vereditos de pacotes, cada destino que a VM abriu e se foi permitido ou negado, trocas de credenciais, detecções de injeção. Código convidado não consegue editá-la, porque código convidado não consegue alcançá-la. Quando o dia acaba, Erase home… e Reset to base… em Resources → Storage levam o profile de volta ao estado logo após a clonagem.

Ligue isso

Se você roda o NemoClaw, aplique a correção da NVIDIA — ela devolve o Ollama ao loopback e põe um proxy com token na frente de tudo que legitimamente precisa sair. O The Hacker News observa que o caminho Windows e WSL não está totalmente coberto por essa versão, então confira sua plataforma no comunicado em vez de supor. Depois vá encontrar todos os outros servidores de modelo na sua máquina e pergunte a eles em que endereço estão ligados. A resposta é 0.0.0.0 mais vezes do que as pessoas esperam, porque contêineres não param de pedir isso.

Num profile, as configurações que valem dois minutos são a lista curta de sempre. Guardrails → Outbound connections com Unmatched traffic em Deny. Credentials → Require approval to use em qualquer coisa que possa gastar dinheiro ou apagar dados. Supply Chain → OSV vulnerability check e filtragem socket.dev ou Delpi se você tiver chave, por cima do portão de idade que já vem ligado. Prompt Injection → os dois detectores, para que o tráfego ao seu modelo local seja lido com o mesmo cuidado que o tráfego a um modelo de nuvem.

Continue rodando o modelo na sua própria máquina. Essa escolha compra prompts que ficam locais, e vale a pena. Ela não diz nada sobre quem mais tem permissão para falar com o servidor que os guarda, o que é uma pergunta sobre um socket, e um socket merece resposta melhor do que uma variável de ambiente configurada para deixar um contêiner feliz. Instale o Bromure Agentic Coding, escolha seu modelo no host, e deixe o convidado com um cano em vez de uma porta.