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

O token estava no mesmo heap

Oren Yomtov, da Accomplish, escapou duas vezes da sandbox do Codex, da OpenAI. Uma das fugas partiu de read-only, o modo mais restrito: JavaScript não confiável chamou v8.getHeapSnapshot() e leu o token de autorização do contexto confiável no heap que os dois compartilhavam. A outra fez o apply_patch ampliar sua própria permissão de escrita até a raiz do sistema de arquivos e depois acrescentou uma linha ao .zshrc. A OpenAI corrigiu as duas em oito dias. A propriedade que as produziu não tem correção: um guarda e o código que ele guarda eram duas variáveis em um único processo. O Bromure Agentic Coding roda o guarda no seu Mac e o agente em uma VM.

O contexto não confiável nunca quebrou a sandbox. Ele pediu ao Node um instantâneo do heap, encontrou ali o token de autorização da própria sandbox como uma string comum, e o usou.

Você aponta o Codex para um repositório que alguém lhe enviou. Você é cuidadoso, então o roda em read-only, o modo em que o agente pode olhar e não pode tocar. O agente lê o código. Parte desse código é uma carga maliciosa, e poucos segundos depois um comando está rodando no seu Mac, fora da sandbox, sem nenhum aviso na tela.

Isso é o Heapjack, uma das duas fugas da sandbox do Codex que Oren Yomtov, da Accomplish, publicou neste mês. O BleepingComputer as noticiou em 20 de setembro. Yomtov reportou as duas à OpenAI em 12 de agosto, e a OpenAI corrigiu em oito dias: o Heapjack no Codex Desktop build 26.818.21641, a outra no Codex CLI 0.149.0. A declaração da OpenAI é do tipo curto: “Agradecemos aos pesquisadores por nos contatarem e compartilharem suas descobertas. Tratamos das duas questões em agosto, mas estamos continuamente fortalecendo nossas sandboxes.”

Por que o primeiro bug existia é a parte que sobrevive à correção.

Um processo, um heap, um token

O Codex Desktop tem uma ferramenta chamada node_repl que executa JavaScript. Ela mantém dois contextos dentro de si: um confiável, que pode pedir ao processo pai que faça coisas reais, e um não confiável, que roda aquilo que o agente está avaliando. A separação entre os dois é a sandbox.

Ambos os contextos vivem no mesmo processo Node. Nas palavras de Yomtov: “Um processo Node, um isolate V8, um heap compartilhado pelos dois contextos. O token é uma string nesse heap.”

O token é como o processo pai distingue um do outro. Uma requisição que chega pelo pipe com o token certo é confiável e é executada; uma sem ele, não. É um UUID aleatório, que é um ótimo segredo enquanto o código do qual você o esconde não puder ler sua memória.

Ele pode. v8.getHeapSnapshot() é uma API pública do Node, uma ferramenta de profiling, e devolve um despejo de todo objeto do heap, strings inclusive. O contexto não confiável a chama, filtra o que tem forma de UUID e termina com um punhado de candidatos. Isolar o verdadeiro exige mais um passo: testar cada um contra o pai e observar o que volta. Um token errado responde “não autorizado”. O certo responde com uma reclamação sobre o resto da requisição. Yomtov fica com a string que produz a segunda resposta.

Com o token em mãos, o contexto não confiável escreve requisições no mesmo pipe stdout que o confiável usa. O pai confere o token, acha-o válido, e roda o comando fora da sandbox. Daí em diante: comandos de sistema via open, o socket do Docker, edições em arquivos de configuração.

Tudo isso a partir de read-only. Citando Yomtov de novo: “Tudo rodou em read-only, o modo de sandbox mais restrito, onde o agente supostamente não consegue escrever nada.”

A fronteira e aquilo que ela limita, num só espaçoum processo Node, um heap V8contexto confiávelpode pedir ao pai que ajaguarda o tokencontexto não confiávelroda o código do repositórioisto é a sandboxo heap que ambos compartilham"a1f3…-…-…" o token de autorização, uma stringv8.getHeapSnapshot()o que o lado não confiável faz1. capturar o heap, guardar os UUID2. "não autorizado" vs erro de validação3. forjar pedidos no pipe stdouto processo paitoken válido → executa, fora da sandboxcomandos no Maca partir de read-only, sem aviso
O Heapjack não quebra a sandbox; ele lê a chave da sandbox. O contexto confiável, o contexto não confiável e o token de autorização que os distingue vivem todos em um único heap V8, dentro de um único processo Node. v8.getHeapSnapshot() é uma API pública de profiling que devolve todas as strings desse heap. O contexto não confiável coleta os candidatos com forma de UUID e identifica o token verdadeiro pela diferença entre uma resposta 'não autorizado' e um erro de validação, e então forja requisições confiáveis no pipe stdout compartilhado. O processo pai valida o token e executa o comando fora da sandbox, em modo read-only, onde nada deveria ser gravável.

Uma permissão de escrita que cresce

O Overpatch, a segunda fuga, é mais simples. O Codex CLI tem uma ferramenta apply_patch para editar arquivos, e ela deduz quanto acesso de escrita precisa a partir dos caminhos citados no patch que recebeu. Os caminhos vêm do modelo, e o modelo está lendo o repositório.

Então um patch cita /tmp em uma entrada, com uma alteração que não faz nada. A alteração falha. A concessão de permissão que ela implicava, não: o acesso de escrita se amplia até a raiz do sistema de arquivos. Uma segunda entrada do mesmo patch então escreve através de um link simbólico dentro de $HOME e acrescenta uma linha ao .zshrc.

Nada escapou ainda. A fuga acontece depois, quando você abre um terminal e seu shell roda aquela linha como você, sem sandbox alguma no caminho. Esta rodou em workspace-write, o modo do dia a dia.

Dois bugs diferentes, dois produtos diferentes, uma frase do artigo que cobre os dois: “A coisa que fazia a imposição estava sentada dentro da coisa que estava sendo imposta.”

O guarda e o guardado, no mesmo espaço de endereçamento

Uma sandbox que vive no mesmo processo que o código que ela contém é uma convenção. Ela se sustenta até alguém olhar com atenção. O token está no heap. O cálculo de permissão lê caminhos fornecidos pelo atacante. O pipe é o mesmo pipe. Cada um desses pontos é corrigível, e a OpenAI corrigiu cada um em oito dias. O próximo bug desse tipo será outra string no mesmo heap.

A conclusão do próprio Yomtov é a arquitetural: “É por isso que a Accomplish roda o agente inteiro dentro de uma VM… Tratamos tudo o que está no convidado como não confiável, root incluído, porque nada que decide o que é permitido está rodando lá dentro com ele.”

Esse é o design que o Bromure Agentic Coding entrega. O agente roda em um convidado Ubuntu 24.04 sob o Virtualization.framework da Apple, o hipervisor que o próprio macOS fornece. O convidado é não confiável por construção, root incluído. O código que decide o que é permitido roda como um processo do host do outro lado de um vsock, e nenhum token dentro do convidado autoriza coisa alguma, porque tudo que vale a pena autorizar está fora do convidado.

uma sandbox dentro do processoum só sistema operacional, uma só contao agentelendo o repositório delesa políticaum token no mesmo heap~/.ssh ~/.aws ~/.zshrc keychain os reposuma string lida da memóriae a separação acabou,tudo acima está ao alcanceuma fronteira no hipervisoro convidado Linux, não confiável, root incluídoo agente, o repositório, seu próprio $HOMEghp_… sk-ant-… substitutasVirtualization.framework + vsocko Mac, onde a decisão aconteceo proxychaves reais, regras,o 451 na saídaa caixa de diálogo"Permitir escrita em … ?"renderizada fora da VM
O que importa é qual máquina avalia as regras. À esquerda: o guarda, o código guardado, o token de autorização e suas chaves SSH, credenciais de nuvem e o .zshrc ficam todos em um só sistema operacional, de modo que ler uma única string encerra a separação. À direita: o agente recebe uma VM Linux cujas credenciais são substitutas que preservam a estrutura, cujos fluxos nas portas 80 e 443 o switch virtual desvia para um proxy no host que o convidado não consegue desligar, e cujo pior desfecho é um disco que você apaga. O comprometimento total do convidado, root incluído, rende ao atacante o convidado.

O que cada fuga encontra no lugar

O Overpatch é o caso concreto, então comece por ele. O truque termina acrescentando texto ao .zshrc em $HOME. Em um workspace Bromure, o $HOME do convidado é home.img, uma imagem ext4 anexada àquela VM. O diretório pessoal do seu Mac não está dentro dele. O que o convidado consegue alcançar no seu Mac são as pastas de projeto que você compartilhou com ele: até oito, escolhidas por você, listadas no editor de workspace. Os dotfiles que seu shell lê quando você abre o Terminal não estão entre elas, a menos que você os coloque ali.

O Heapjack é o caso mais difícil. Nada no Bromure detecta um truque de instantâneo de heap dentro do convidado, e nada deveria. O que muda é onde a fuga chega. No Codex, o código escapado caía na conta que guarda suas chaves SSH e suas credenciais de nuvem. Em um workspace Bromure, ele cai em uma VM Linux onde:

As credenciais são substitutas

sk-ant-api03-brm-…, uma string com forma de ghp_, um perfil ~/.aws cujo segredo é inválido. Elas preservam a estrutura, então as ferramentas as aceitam, e não valem nada lá na frente. Os valores reais vivem na memória do proxy no host e são trocados na saída, restritos ao host exato ou a seu subdomínio.

Levar uma para fora dispara o alarme

O proxy examina cabeçalhos e corpos de toda requisição de saída em busca das substitutas do próprio workspace. Uma substituta indo para onde não foi cunhada é bloqueada com HTTP 451, sem um byte encaminhado. A VM pausa, e uma linha vermelha Intermediação de credenciais aparece na Linha do Tempo de Segurança.

A rede é uma lista que você escreveu

A saída é um conjunto ordenado de regras que termina em default allow ou default deny. O switch virtual desvia os fluxos das portas 80 e 443 da VM para o proxy no host, sem nenhuma variável de ambiente envolvida e sem nada que o convidado consiga desligar. As edições se aplicam imediatamente a uma sessão em andamento.

Um push é uma pergunta, feita no Mac

As credenciais de forja Git carregam uma política de escrita, e novos workspaces usam por padrão Perguntar antes de escrever. git push é uma escrita. A caixa de diálogo nomeia a operação exata e é renderizada na sua área de trabalho. Um atacante dentro da VM não consegue clicar nela.

Quando dá errado, você se recupera com um item de menu. O fluxo de comprometimento apaga o disco e o home do workspace e mantém suas configurações, tokens e chaves SSH. As credenciais reais nunca saíram do seu Mac, então você não rotaciona nada. Redefinir disco re-clona a imagem do sistema. Nenhuma ação do Bromure toca suas pastas de projeto compartilhadas, e o aviso diz isso.

O modo mais restrito foi o que perdeu

O desenvolvedor desta história fez tudo certo. Escolheu a configuração mais restrita que a ferramenta oferece, de propósito, porque o repositório não era dele. read-only é um recurso real e fez um trabalho real, até que uma string em um heap compartilhado a transformou em sugestão.

Modos, listas de permissão e avisos de aprovação valem a pena, e cada um deles presume que o código que os impõe está fora do alcance do código que ele restringe. Isso é uma afirmação sobre layout de memória, e você pode ir verificar. Yomtov verificou o Codex duas vezes, em dois produtos, e a resposta voltou negativa nas duas.

Então rode o agente em algum lugar onde a resposta deixe de importar. Dê root a ele, deixe que instale o que quiser, deixe que leia o repositório que alguém lhe enviou, e mantenha cada credencial, cada decisão de destino e cada confirmação de push do outro lado de um hipervisor, onde a próxima fuga chega em uma máquina que você já estava disposto a jogar fora.