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

A pré-visualização podia ler a sua pasta pessoal

Em 14 de outubro, a Noma Labs revelou o SneakPeek, uma falha no painel de pré-visualização do Claude Code Desktop. Um arquivo HTML inflado para mais de 256 KB, disfarçado de relatório de cobertura de testes, era carregado do disco sem política de scripts, e o JavaScript dele lia chaves SSH, segredos da AWS, kubeconfigs e os próprios tokens OAuth do Claude na pasta pessoal do desenvolvedor, depois os enviava para fora com um beacon em segundo plano. A Anthropic deu nota CVSS 8.7 à falha e a corrigiu em agosto. No Bromure Agentic Coding, as páginas são renderizadas em uma VM separada e descartável que não tem nenhum dos seus arquivos, e a pasta pessoal do agente guarda substitutos no lugar de chaves.

O repositório parecia normal. O arquivo era um relatório de cobertura. O desenvolvedor pediu ao agente que desse uma olhada, e o painel de pré-visualização fez o resto.

Um desenvolvedor clona um repositório e pede ao Claude Code que olhe o relatório de cobertura de testes que está nele. É um pedido rotineiro. O relatório é um arquivo HTML. Quando o Claude lê ou edita um arquivo HTML no Claude Code Desktop, um painel de pré-visualização o renderiza ao lado da conversa, sem perguntar. Esse relatório carrega um script. Em segundos, o script leu a chave privada SSH do desenvolvedor, as chaves da AWS, os tokens do npm e do GitHub, as credenciais do Kubernetes e os próprios tokens OAuth do Claude, e mandou tudo para um servidor controlado pelo atacante.

Essa é a prova de conceito que a Noma Labs publicou na quarta-feira, 14 de outubro, com o nome de SneakPeek. A CSO Online noticiou no mesmo dia. A Anthropic deu à falha nota CVSS 8.7 (alta) e tratou o impacto como equivalente a execução de código no host. A Noma a reportou em 4 de julho. A correção chegou por atualização automática em 17 de agosto, no Claude Desktop 1.30096.1, depois que um primeiro patch foi contornado. Um aviso do GitHub e um CVE estão pendentes.

O bug está corrigido. O cenário que fazia valer a pena procurá-lo continua na maioria dos laptops de desenvolvedores: algo que renderiza conteúdo não confiável, rodando como você, ao lado de tudo o que é seu.

Três defesas e um limite de tamanho

O painel de pré-visualização é um painel do Chromium dentro do app Electron. A Noma contou três defesas em volta dele e encontrou uma brecha em cada uma.

Uma política de scripts para arquivos pequenos. Arquivos com menos de 256 KB eram carregados como URLs data: em base64 com uma Content Security Policy, uma lista de regras que diz ao navegador quais scripts podem rodar. Essa política bloqueava scripts. Arquivos com mais de 256 KB eram gravados em um arquivo temporário e carregados do disco como URL file://, e a política não valia para esse caminho. Inflar um relatório para mais de 256 KB é trivial. Relatórios de cobertura reais costumam ter esse tamanho.

Uma origem com livre acesso ao disco. A origem de uma página decide o que ela pode ler. Uma URL data: ganha uma origem opaca que não lê nada. Uma página carregada de file:// ganhava, nas palavras da Noma, “acesso de mesma origem a todo o sistema de arquivos local da vítima”. O script descobria a pasta pessoal a partir do caminho do arquivo temporário e lia arquivos com fetch() e XMLHttpRequest.

Um filtro que não estava ligado. O painel tinha um filtro de requisições feito para bloquear o acesso file://. Ele entrava em ação para servidores de desenvolvimento que passavam pelo proxy e ficava desligado para HTML estático renderizado do disco.

Para tirar os dados, o script chamava navigator.sendBeacon(), uma API web que dispara em segundo plano um HTTP POST de mão única. Nada no app limitava para onde o painel podia enviar requisições. O relato da Noma acrescenta o endereço IP real do desenvolvedor, obtido via WebRTC.

O primeiro patch da Anthropic, em 24 de julho, bloqueava fetch() e XMLHttpRequest para caminhos file:// a partir da página. No mesmo dia, a Noma moveu as leituras para um Web Worker iniciado a partir de uma URL blob:, um contexto que o patch não cobria, e os três defeitos voltaram a funcionar. A correção final, em agosto, fechou juntos a brecha da política, o carregamento via file:// e o caminho do worker. O pesquisador da Noma Eli Ainhorn resumiu para a CSO: “As proteções eram reais, só não cobriam todos os caminhos.”

No laptop: uma olhada no relatório, um beacon para foraRepositório públicosrc/ tests/coverage/index.htmlinflado: +256 KBum script oculto“Olhe o relatóriode cobertura.”Painel de prévia, rodando como você no Macabaixo de 256 KB: URL data:, sem scriptsacima de 256 KB: URL file://, sem política,mesma origem que o disco inteirofetch ~/.ssh/id_ed25519fetch ~/.aws/credentialsfetch ~/.kube/configfetch ~/.docker/config.jsonfetch tokens OAuth do Claudesem pedir permissão, sem cliqueServidor do atacantesendBeacon POST-----BEGIN OPENSSHaws_secret_access…client-key-data:…tokens npm e GitHube o seu IP real,via WebRTCO agente não rodou nenhum comando. Quem leu foi o renderizador, com o acesso da sua conta aos seus arquivos.
O SneakPeek em um laptop. Um relatório de cobertura inflado para mais de 256 KB escapa da política de scripts, é carregado do disco com acesso à pasta pessoal inteira, lê as chaves do desenvolvedor e as envia por beacon. Tudo o que o desenvolvedor fez foi pedir ao agente que olhasse um arquivo.

Não foi o agente

O SneakPeek não usa injeção de prompt. O agente leu um arquivo que você pediu para ele ler, e o app em volta dele renderizou esse arquivo. O script rodou no processo de renderização da pré-visualização, com o acesso do app ao seu disco.

Crystal Morin, da Sysdig, disse à CSO que os logs no nível do agente podem não registrar essa execução, então os defensores teriam de vigiar leituras de arquivos e conexões de saída na camada de runtime. A transcrição da sua sessão mostraria um pedido inofensivo e uma resposta inofensiva.

Um modelo cuidadoso, regras de permissão rígidas e uma pessoa aprovando cada comando não teriam mudado nada. O renderizador estava no Mac, rodando como você, ao lado da sua pasta pessoal e de todas as credenciais que você juntou ali.

O conselho de Ainhorn à CSO mira essa segunda parte: manter credenciais de longa duração fora das máquinas dos desenvolvedores e usar tokens de escopo restrito e vida curta.

O mesmo relatório em uma máquina Bromure

No Bromure Agentic Coding, cada agente roda dentro da própria VM Linux no seu Mac, chamada máquina, e o navegador embutido que renderiza páginas web roda em uma segunda VM descartável ao lado. Clone o repositório da Noma em uma máquina e peça ao Claude Code que olhe o relatório de cobertura.

Ler o relatório é ler texto

O agente lê coverage/index.html como lê qualquer arquivo: como texto no seu contexto. Nada no chat o renderiza. Se você clicar no arquivo no painel Files, a visualização de detalhe mostra o HTML como código-fonte destacado, do mesmo jeito que mostra um .py ou um .json. Nenhum script da página roda. Arquivos com mais de 512 KB nem ganham prévia. Um relatório inflado para mais de 256 KB para driblar uma política de scripts aparece como 300 KB de marcação.

A renderização acontece em outra máquina

Quando você quer mesmo ver o relatório renderizado, o agente serve a pasta a partir da máquina dele e a abre no painel do navegador, no endereço IP da máquina. O navegador é um Chromium completo e roda na própria VM auxiliar. Como diz o manual, “um arquivo que o agente grava no diretório pessoal dele não está no disco do navegador”.

O script roda e não encontra nada para ler. A origem da página é http://192.168.64.x:8000, uma origem web comum. O Chromium não a deixa ler caminhos file://. Um script que passasse por isso, via worker ou bug do navegador, cairia em um clone efêmero copy-on-write da imagem do navegador: sem ~/.ssh, sem ~/.aws, sem kubeconfig, sem token OAuth, sem nenhum dos seus projetos. A pasta pessoal que ele deduz de um caminho temporário pertence a uma VM do Chromium vazia. O beacon sai vazio.

Por padrão, o navegador não guarda perfil. Stay signed in to websites fica desligado, então cookies e logins somem quando o navegador fecha, e um navegador oculto é destruído depois de cinco minutos. O WebRTC continua desligado a menos que você permita a câmera ou o microfone nas configurações Browser da máquina, então o truque do endereço IP não tem o que chamar.

A pasta do agente guarda substitutos

Agora aponte o mesmo script para a VM do próprio agente. Digamos que o comando de testes do repositório grave e abra o relatório sozinho, de modo que o código rode onde o agente trabalha. Percorra a lista da Noma nessa pasta pessoal:

  • Chave privada SSH. Não existe. O SSH_AUTH_SOCK é sustentado por uma ponte de agente até o Mac. A VM pode listar identidades e pedir assinaturas, e os bytes da chave privada nunca saem do host. ls ~/.ssh/id_* não acha nada.
  • AWS. O ~/.aws/config aponta para um helper que entrega o ID real da chave de acesso com um segredo falso de 40 caracteres. O Mac assina de novo cada requisição com o segredo real a caminho da AWS.
  • Kubernetes. O ~/.kube/config é sintético. O kubectl fala com o proxy no Mac, que guarda as credenciais reais do cluster.
  • Tokens do Docker, git e npm. ~/.docker/config.json e ~/.git-credentials guardam substitutos com o formato dos reais. Tokens que você importa, de um .env ou à mão, viram substitutos restritos aos seus hosts.
  • Os tokens OAuth do Claude. Ficam no Mac. O agente roda com uma chave de mentira, e o proxy coloca o token de acesso real na conexão para as requisições à Anthropic.

Os substitutos também são armadilhas. O proxy no Mac examina cada requisição que sai da máquina, cabeçalhos e corpo, em busca de um substituto indo para um host para o qual não foi emitido. Um beacon levando a senha brm-docker-… ou o token falso do GitHub para o servidor do atacante recebe HTTP 451. Nada é encaminhado, a VM é pausada e um alerta com o título This environment may have been compromised se abre no Mac, com uma prévia do token, o nome da credencial, o host a que ela pertence e o host para onde foi enviada. A detecção vira uma linha vermelha no Security Timeline. Defina Unmatched traffic como Deny no firewall Outbound connections da máquina, e uma conexão a um host que o projeto não usa é recusada antes de tudo isso.

Isso responde ao ponto de Morin sobre a camada de runtime. As verificações ficam no Mac, na conexão, fora do agente e fora de qualquer coisa que renderize dentro da VM.

No Bromure: renderizador sem arquivos, agente sem chavesVM da máquina (agente)index.html lido como textopainel Files: código destacadoPasta pessoal~/.ssh/id_* (nenhuma)~/.aws helper, segredo falso~/.kube/config sintéticodocker, git substitutos brm-…chave Claude de mentiraserve coverage/ na :8000nenhum segredo real para lerVM do navegador (descartável)http://192.168.64.x:8000o script rodaleituras file:// recusadasdisco: clone novo, sem seus arquivossem perfil, WebRTC desligadosendBeacon(…) → vaziodestruída 5 minutosdepois de ocultaO Maco proxy põe a chave realsó para o host delasubstituto ao host errado:451, VM pausada, alertafirewall: host estranho negadoSecurity Timeline:linha vermelha, dois hostsNo Mac, fora das duas VMsChaves privadas SSH · segredos AWS · credenciais de cluster · tokens de registry e git · tokens OAuth do Claude · Security Timeline
O mesmo relatório no Bromure. O agente o lê como texto e o painel Files o mostra como código-fonte. Quando ele é renderizado, isso acontece em uma VM de navegador descartável sem nenhum dos seus arquivos. A pasta pessoal do agente guarda substitutos, e um substituto enviado ao host errado para a máquina.

A correção e a próxima pré-visualização

A Anthropic corrigiu o SneakPeek em agosto e a Noma testou de novo. Seis semanas se passaram entre o reporte e a correção, e o primeiro patch caiu diante de um Web Worker no mesmo dia em que saiu. Correções de renderizador costumam ser assim. Um motor de navegador tem muitas maneiras de iniciar um script e muitas maneiras de fazer uma requisição, e cada defesa precisa cobrir todas.

As ferramentas de programação não param de ganhar painéis que renderizam coisas: prévias de HTML, Markdown com conteúdo incorporado, saídas de notebooks, servidores de desenvolvimento, capturas de tela do app que o agente acabou de construir. Cada um é um renderizador apontado para conteúdo que o desenvolvedor não escreveu, de repositórios clonados um minuto antes. Em um laptop, cada um roda ao lado da pasta pessoal.

No Bromure, o painel que renderiza páginas roda em uma VM sem nenhum dos seus arquivos, e a máquina onde o agente trabalha guarda substitutos. Um bug de renderizador ali expõe um Chromium descartável e um punhado de strings que nenhum servidor vai aceitar.

Código-fonte, não uma página

O agente lê HTML como texto, e o painel Files o mostra como código-fonte destacado. Abrir um relatório para revisá-lo não roda nenhum dos scripts dele.

Um navegador sem nada no disco

As páginas são renderizadas em uma VM do Chromium separada e descartável. Os arquivos do agente e os do seu Mac não estão no disco dela, e ela esquece tudo ao fechar.

Uma pasta pessoal cheia de substitutos

Nenhuma chave privada SSH, um segredo da AWS falso, um kubeconfig sintético, substitutos de tokens de registry e git. As credenciais reais ficam criptografadas no Mac.

Um alarme na conexão

Um substituto enviado a um host para o qual não foi emitido recebe um 451. A VM é pausada, um alerta nomeia os dois hosts e o Security Timeline guarda o registro.

Olhe o relatório

“Olhe o relatório de cobertura” é um dos pedidos mais comuns que se faz a um agente de programação. Desenvolvedores vão continuar perguntando sobre arquivos de repositórios que não escreveram, e as ferramentas vão continuar renderizando esses arquivos para ajudar.

Peça em uma máquina Bromure, onde a página renderiza em uma VM sem nada para ler e as chaves do agente são substitutos. Para saber mais sobre os substitutos e o alarme que eles disparam, leia O agente vazou a própria chave e A chave era o seu arquivo .env.