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

Bastava estar logado

Um desenvolvedor que estava liberando espaço em disco encontrou 700 MB num diretório oculto e puxou o fio: o ZCode, da Z.ai, estava empacotando em tar toda a sua árvore de trabalho, 42.411 arquivos com 86,6% deles em .git, criptografando com uma chave que só a Z.ai tem, e postando direto num bucket da Alibaba Cloud. Nenhum atacante, nenhum CVE, nenhum malware. O gatilho era estar logado. O cliente criptografava o pacote, então nenhum scanner conseguia ler, e os únicos fatos que sobravam eram o destino e o tamanho. Esses são dois dos campos que o Bromure Agentic Coding anota para cada requisição que um workspace faz, num fio que a ferramenta dentro da VM não escolhe.

Sua ferramenta de programação não precisa estar comprometida para mandar seu código-fonte para algum lugar. Basta um padrão de fábrica. Confiar no fornecedor não é a questão. Pergunte se você ficaria sabendo.

Você está liberando espaço num disco cheio. Um diretório oculto na sua pasta pessoal está ocupando mais de 700 megabytes, e você não lembra de ter instalado nada desse tamanho. Dentro dele, numa pasta chamada checkpoints, há um único arquivo de 313 MB com a extensão .enc e, ao lado, um registro de metadados que diz failureCount: 564.

Quinhentas e sessenta e quatro tentativas de algo que você nunca pediu.

Setecentos megabytes e um fio para puxar

Foi aí que o desenvolvedor chinês conhecido como ferstar começou, em 18 de setembro, e o relato de engenharia reversa do que ele encontrou vale ser lido por inteiro. O diretório pertencia ao ZCode, o cliente de programação com IA para desktop da Z.ai, a empresa por trás dos modelos GLM. CSO Online e InfoWorld trazem os dois o resumo dele do comportamento, palavra por palavra:

“Sempre que você está logado, o ZCode empacota silenciosamente todo o seu workspace — histórico .git completo, cache de assets LFS, reflogs e configurações globais do aplicativo —, criptografa, e envia direto para o Aliyun OSS.”

O gatilho está na primeira oração. Não uma pergunta que você fez, não o recurso de indexação que você ligou: um token de sessão válido, que o processo de captura espera, e nada mais. Uma única sessão registrou 62 eventos de captura nos logs dele. Quando ele apagou o pacote, o cliente construiu outro em menos de meia hora e incrementou o contador de falhas.

O que havia na caixa

O snapshot que ele desmontou continha 42.411 arquivos, 345 MB antes da criptografia e 313 MB depois. O que compunha esses 345 MB:

  • .git/lfs/: 196,1 MB, 56,8%
  • .git/objects/: 102,2 MB, 29,6%
  • .git/logs/: 0,6 MB, 0,2%
  • todo o resto, ou seja, código e documentação de verdade: 46,2 MB, 13,4%

86,6% do upload era .git. O pacote levava cada objeto do histórico do repositório, cada binário grande que o cache LFS tinha baixado, e o reflog, que lista cada branch em que você esteve e cada commit que você fez e depois desfez. Quem lê sua árvore de trabalho descobre o que você está entregando; quem lê seu .git descobre o que você tentou, o que você reverteu, e quem fez o commit.

Um token de login, quatro passos, nenhum atacanteSua máquina, logadadiretório de trabalho.git/lfs, .git/objects, .git/logscódigo, docs, configs globais42.411 arquivos · 345 MBDuas chaves em Configuraçõesotimização da experiência → treinamentoindexação de snapshot → índice no servidornenhuma delas controla o empacotamento1 · zcode.z.aidevolve uma assinatura OSSe uma chave pública RSAa metade privada fica no servidor2 · empacotado e selado localtar.gz → AES-256-CTRchave ← RSA-OAEP-SHA256313 MB, ilegíveis para você3 · POST para o Alibaba Cloud OSSbucket zcode-proddireto — não pelos servidores da Z.aium objeto por evento de captura4 · callback do OSSavisa o backend que o objeto chegouo snapshot fica registrado eindexável do lado delesO que disparava tudoUm token de login válido. Não um prompt, não uma pergunta, não o recurso de indexação.62 eventos de captura numa única sessão. Apague o pacote e ele é reconstruído em trinta minutos.failureCount: 564
Como o pacote saiu da máquina. O cliente ZCode pediu ao zcode.z.ai credenciais de upload e recebeu uma assinatura de formulário OSS junto com uma chave pública RSA; empacotou o diretório de trabalho em tar, criptografou o pacote localmente com AES-256-CTR sob uma chave envelopada com aquela chave pública, e postou o resultado direto no armazenamento de objetos da Alibaba Cloud, contornando os próprios servidores de aplicação da Z.ai, com um callback do OSS avisando o backend de que o objeto havia chegado. A chave privada correspondente nunca chega ao cliente, então o usuário não consegue descriptografar o próprio pacote. Um token de login válido é o gatilho, e não qualquer uso do assistente. Uma sessão registrou 62 eventos de captura, e apagar o arquivo provocava um novo empacotamento em cerca de trinta minutos.

As duas chaves governavam o lado deles

O ZCode vinha com duas configurações que um desenvolvedor cuidadoso teria achado e desligado: uma para otimização da experiência, outra para indexação de snapshots do repositório. As duas faziam algo real, e nenhuma fazia o que o nome sugere.

A primeira governava se a Z.ai estava autorizada a usar os dados. A segunda governava se o servidor indexava o snapshot depois de ele chegar. O empacotamento e o upload rodavam de todo jeito, porque o sidecar de captura sobe assim que o provedor de tokens lhe entrega um JWT válido, sem nenhuma preferência do usuário em lugar nenhum do caminho. Desligue as duas chaves e o cliente ainda empacotava seu repositório, ainda criptografava, ainda postava no bucket, e prometia discrição sobre o que acontecia depois disso.

O bug de um fornecedor, mas o formato se repete em todo lugar. Um controle que mora dentro do programa que ele restringe é um pedido. Ele roda no mesmo código que precisaria estar errado para você precisar dele.

Um snapshot: 42.411 arquivos, 345 MB antes da criptografia.git/lfs — 196,1 MB.git/objects — 102,2 MBcódigo — 46,2 MB56,8%29,6%13,4%86,6% do que saiu da máquina era o histórico do repositório — objetos, cache LFS, reflogsSó o reflog já é a lista de cada branch em que você esteve e cada commit que você fez e depois desfez.Os arquivos de configuração global do aplicativo viajaram junto.
345 MB, e só 46 deles eram código. O snapshot que ferstar recuperou continha 42.411 arquivos: 196,1 MB de cache do Git LFS, 102,2 MB de objetos do Git e 0,6 MB de reflogs, 86,6% da carga, contra 46,2 MB de código-fonte e documentação de verdade. Uma ferramenta que faz upload do seu diretório de trabalho faz upload de tudo que você já escreveu naquele repositório, incluindo as branches que você abandonou.

Nada no fio conseguia ler aquilo

O cliente criptografava cada pacote com AES-256-CTR antes de ele sair da máquina e envelopava a chave simétrica com uma chave pública RSA-OAEP-SHA256 entregue a ele pelo zcode.z.ai. A chave privada correspondente nunca desce. ferstar não conseguia abrir o próprio pacote; a única parte que conseguia era a que tinha o bucket.

Isso descarta toda defesa sua que funcione lendo bytes. Um scanner de segredos não acha chave alguma da AWS naquele tarball, e a inspeção profunda de pacotes vê uma sessão TLS para um endpoint de armazenamento carregando texto cifrado envelopado em texto cifrado.

Dois fatos sobrevivem: para onde foi e que tamanho tinha. Um POST de 313 MB para um host de armazenamento de objetos numa região onde seu time não faz deploy chama atenção, desde que alguém anote onde suas ferramentas se conectam e quanto elas enviam. Ninguém anotava, e é por isso que achar isso exigiu arqueologia de espaço em disco e uma semana de engenharia reversa em vez de uma olhada num log.

Onde fica a saída

O Bromure Agentic Coding coloca o agente e sua cadeia de ferramentas dentro de uma VM Ubuntu no seu Mac, e dá a essa VM exatamente um caminho para alcançar a internet: um proxy man-in-the-middle rodando no host, fora do guest, onde nenhum processo da VM pode reconfigurá-lo.

Isso não pede nada ao software. O switch de rede virtual desvia de forma transparente os fluxos da VM nas portas 80 e 443 para o proxy do host, sem nenhuma variável de ambiente necessária e sem nada que o guest possa desfazer, ligado por padrão em todo workspace. Uma ferramenta que ignora HTTPS_PROXY, abre seu próprio socket e fala direto com um endpoint de armazenamento ainda passa por ali, porque a interceptação mora no switch em que o guest está plugado, e não no guest.

Aí quatro coisas acontecem com um POST de 313 MB que ninguém pediu.

Toda requisição deixa registro

Toda requisição pelo proxy deixa um registro de trace: timestamp, host, porta, método, caminho, status, latência, e bytes de requisição medidos antes de qualquer troca de credencial. Você não precisa descriptografar um pacote para notar um upload do tamanho de um repositório para um host onde você nunca fez deploy: o tamanho e o destino são colunas. Ordene por uma, filtre pela outra.

Nunca sai do Mac

O firewall de egresso por workspace é uma tabela de regras ordenada (ação, protocolo, host ou CIDR, portas, métodos HTTP) terminando em default allow ou default deny. Rode um workspace no estilo lista de permissões e o upload não tem para onde ir. Dois lugares aplicam as regras: o switch compara o IP de destino e o hostname captado do DNS, o proxy compara o nome de servidor TLS de novo, e suas edições chegam às sessões em execução na hora.

Todo veredito é uma linha

Toda decisão do firewall cai na Security Timeline, então uma ferramenta que fica tentando de novo, 564 vezes digamos, aparece como uma negação repetida contra um host nomeado em vez de um disco enchendo devagar. O padrão que levou uma semana para reconstruir é justamente o que aquela janela existe para mostrar.

As configs que ele empacota são iscas

“Configurações globais do aplicativo” é a expressão que deveria preocupar você mais na frase de ferstar. Dentro de um workspace Bromure, ~/.git-credentials, ~/.docker/config.json, ~/.kube/config e ~/.aws/config existem e funcionam, e cada valor dentro deles é um falso que preserva a estrutura. Nenhum arquivo, variável de ambiente ou processo da VM jamais guarda uma chave de API, um token OAuth, um segredo da AWS ou uma chave privada SSH de verdade.

O escopo faz o resto. Um workspace é um projeto, ou uma fronteira de credenciais; dois workspaces nunca compartilham uma VM, e a pasta pessoal do guest é um arquivo de imagem próprio. O que uma ferramenta empacota dentro de um workspace é aquele workspace, o que deixa de fora os outros quarenta repositórios do seu Mac, o ~/.aws/config do seu host, e suas chaves SSH. As chaves nunca chegam a um guest: a assinatura acontece no host por uma ponte vsock, e os bytes privados ficam lá.

A ferramenta no MacPasta pessoal~/work/* : cada repositório, cada .git~/.aws/config, ~/.docker, ~/.ssh : reaisA ferramenta escolhe o que ler e para onde mandar.A ferramenta aplica suas próprias configurações.313 MB, criptografados, para um bucketNenhum registro do destino. Nenhum registro do tamanho.Como você fica sabendoSeu disco enche, e você começa a engenharia reversa.A ferramenta num workspaceVM do workspace — guest Ubuntu, um projeto/home/ubuntu — este repo, este checkout~/.aws/config, ~/.docker — placeholdersA ferramenta ainda escolhe o que ler e mandar.Ela não escolhe a rota.:80 e :443 desviadas para o proxy do hosthost · método · caminho · bytes → tracedeny web *.aliyuncs.com · default denyRecusado no fio, e escrito na Security Timeline.
A mesma ferramenta, dois lugares. Rode-a direto no Mac e ela fica ao lado de cada repositório que você tem, de cada arquivo de credenciais na sua pasta pessoal e de uma rota sem intermediários para a internet; o upload é uma sessão TLS como qualquer outra, e o pacote está criptografado, então nada que leia conteúdo ajuda. Rode-a num workspace Bromure e seus fluxos nas portas 80 e 443 são desviados para um proxy no host que ela não consegue reconfigurar: o proxy registra o destino e a contagem de bytes, as regras de egresso decidem se a requisição sai, e os arquivos de configuração que ela empacotou guardam placeholders cujos valores reais nunca entraram na VM.

O que a Z.ai fez depois

A Z.ai se desculpou no dia em que ferstar publicou. A empresa desativou o fluxo de upload no ZCode v3.14.0, que a InfoWorld informa ter saído em 19 de setembro listando uma correção para um problema anormal de upload de repositório, e chamou a firma de segurança NSFOCUS para auditar o resultado. A NSFOCUS confirmou que “todos os objetos de dados no bucket zcode-prod do Alibaba Cloud OSS, assim como o próprio bucket, foram apagados”, e a Z.ai diz que depois da auditoria “nenhum caminho funcional capaz de disparar a geração de snapshots locais de repositório ou de transmitir arquivos locais para fora foi identificado”, e que o código “nunca foi usado para treinamento de modelos”. Depois eles abriram o código: cliente, backend, UI compartilhada, Agent CLI e runtime, sob Apache-2.0, no GitHub. Isso é melhor que a norma do setor, e significa que a próxima pessoa que quiser verificar não vai precisar de um desassemblador.

Cris Thomas, da Semgrep, citado na cobertura da CSO, nomeou a categoria: “Isso não é realmente um problema de modelo de IA, é um problema antiquado de arquitetura de segurança.” Ele quer que os fornecedores divulguem o que sai da máquina, para onde vai e por quanto tempo fica guardado, “com as permissões mínimas ligadas por padrão, não as máximas”. Nenhum cliente consegue impor isso de fora.

O padrão de fábrica é o modelo de ameaça

Ninguém atacou ninguém aqui. Ninguém roubou uma credencial, envenenou um pacote ou convenceu um modelo de nada. Um produto fez o que seus autores construíram para ele fazer, e o que eles construíram para ele fazer foi mandar seu repositório para algum lugar onde você não conseguia lê-lo. O incidente é um padrão de fábrica, e quem escolhe os padrões são pessoas cujos interesses correm ao lado dos seus.

É esse o argumento para colocar a fronteira fora da ferramenta. Você não precisa acreditar que seu assistente de programação é hostil; você só precisa notar que não consegue auditar o que não vê, e que o painel de configurações do fornecedor é um lugar estranho para guardar a chave. Rode o agente e sua cadeia de ferramentas onde cada conexão que eles abrem cruza um fio que é seu, onde o registro dessa travessia é escrito no seu Mac, e onde a regra que barra um upload de 313 MB é uma linha que você digitou em vez de uma caixinha que outra pessoa respeitou.

Aí “o que a minha ferramenta de programação está mandando?” é uma janela que você abre numa terça-feira à tarde, não um projeto de pesquisa.