Como um agente de programação podia ler seu arquivo .env de forma indireta, e como o zn-gate 1.12.0 fechou esses caminhos?
Para o zn-gate 1.5.0 a 1.11.0, a resposta é sim, de algumas formas específicas. Os hooks dos agentes recusavam o comando óbvio que lê um arquivo .env, mas julgavam o caminho como estava escrito no comando, relativo à pasta atual. Um comando que chegasse ao mesmo arquivo por outro caminho podia rodar. A versão 1.12.0 resolve essas formas antes de decidir, verifica todos os comandos de uma cadeia e recusa o que não consegue verificar dentro de um limite de tempo. O problema é o GHSA-rccq-h3xc-j9qj, de gravidade média. A equipe do zn o encontrou em uma revisão interna.
Este artigo descreve as classes de caminhos no nível do aviso publicado, como a correção foi desenhada e o que diríamos a qualquer pessoa que escreva uma proteção para agentes de programação. Os únicos comandos citados são os exemplos já publicados no changelog da 1.12.0; ele não acrescenta nenhuma forma nova nem prova de conceito. A prova de conceito completa continua sem ser publicada enquanto os usuários atualizam.
O que a proteção de leitura do zn-gate faz?
O zn-gate instala hooks no Claude Code, no Codex CLI, no Cursor e no OpenCode. Antes de o agente executar um comando de shell, o hook recebe o texto do comando e responde permitir ou recusar. Uma de suas tarefas é recusar comandos que leriam um arquivo protegido, para que o conteúdo nunca chegue ao modelo. Os arquivos protegidos são os arquivos .env que guardam valores em texto puro e arquivos de credenciais como ~/.aws/credentials.
A proteção não executa o comando. Ela lê o texto, encontra os nomes de arquivo que o comando menciona e decide. Isso é uma verificação estática sobre uma linguagem que não é estática: o shell expande, substitui e reinterpreta o texto enquanto roda. A distância entre o que a proteção vê e o que o shell faz é de onde vem cada um dos caminhos abaixo.
Um arquivo .env cujos valores foram trocados por marcadores com zn env shadow não é um alvo protegido do mesmo jeito. Ele contém nomes e marcadores no formato __ZN_<NAME>__, então lê-lo por qualquer caminho não mostra nenhum valor. É por isso que o shadow importa para o que vem a seguir.
Quais caminhos a proteção deixava passar?
O aviso descreve estas formas. Aqui estão elas como classes.
| Classe | O que deu errado | Exemplo do changelog público |
|---|---|---|
| Curingas | Um padrão em um caminho não era expandido antes da verificação, então a proteção procurava um arquivo com o nome do padrão e não encontrava nada | cat app*/.env |
| Mudança de diretório | A verificação resolvia os caminhos a partir da pasta em que o hook roda, e não da pasta para a qual o comando tinha se movido | cd app && cat .env |
| Caminhos montados enquanto o comando roda | Uma variável ou uma substituição de comando fornecia parte do caminho, e o valor dela não existia no momento da verificação | um caminho que começa com uma variável ou com uma substituição de comando |
| Algumas formas de aspas e redirecionamento do shell | O shell tem várias maneiras de escrever a mesma string e de abrir um arquivo para leitura, e a proteção entendia só as mais simples | |
| A cópia guardada no git | Um .env commitado uma vez, ou mantido no índice, pode ser impresso a partir do repositório mesmo que não esteja mais no disco |
git show HEAD:.env |
| Cadeias muito longas, verificadas só em parte | Apenas os primeiros comandos de uma cadeia longa eram examinados, então uma leitura no final não era verificada, para qualquer arquivo protegido | |
| Uma verificação mais lenta que o timeout do hook | Cada agente espera um tempo limitado pelo hook; um comando que demorava mais para ser verificado do que isso rodava sem verificação |
As cinco primeiras são formas de nomear um arquivo que a proteção não resolvia. As duas últimas são diferentes por natureza: elas não escondem o arquivo, elas fazem a verificação não acontecer. As formas de cadeia longa e de timeout valiam para todos os arquivos protegidos, inclusive arquivos de credenciais fora do .env.
Por que um curinga é um problema para uma verificação de caminho?
Porque a proteção e o shell discordam sobre o que o texto significa. O shell transforma um padrão na lista de caminhos que casam com ele antes de o comando começar. Uma proteção que trata o padrão como um nome literal pergunta se existe um arquivo chamado exatamente isso, e a resposta é não. O comando então é permitido, e o shell o expande para um arquivo real. Na 1.11.0, o caminho simples era recusado e o mesmo arquivo alcançado por um padrão no nome de uma pasta não era.
Por que mudar de diretório importa?
Os caminhos em um comando significam algo relativo à pasta atual, e um comando pode mudá-la no meio. A proteção resolvia o que lia em relação à pasta em que o agente começou. Quando um comando se move, essa base fica errada. O mesmo vale para um .env que já tem shadow na pasta inicial: a proteção podia ficar satisfeita com o arquivo com shadow enquanto o comando lia outro.
O que há de errado com caminhos montados enquanto o comando roda?
O valor deles não existe quando a proteção olha. Uma variável definida antes na cadeia, ou a saída de uma substituição de comando, só vira texto quando o shell a executa. A proteção vê um espaço reservado e não tem como saber no que ele vai se transformar.
E as aspas, o redirecionamento e a cópia no git?
Os shells aceitam muitas grafias para a mesma string, e várias maneiras de abrir um arquivo para leitura além de nomeá-lo como argumento. Uma proteção que normaliza apenas a grafia comum deixa passar as outras. O caminho do git é uma observação diferente: um arquivo protegido tem mais de um lugar para viver. A cópia da árvore de trabalho é aquela em que todo mundo pensa. A cópia commitada e o histórico podem ser impressos com comandos do git, e continuam lá depois que o arquivo é apagado ou ignorado.
Por que cadeias longas e verificações lentas importam mais que os outros?
Porque eles não dependem de como o arquivo é nomeado. Se a proteção examina apenas os primeiros comandos de uma cadeia longa, tudo o que vem depois desse ponto fica fora da verificação. Se a proteção precisa de mais tempo do que o agente espera, o agente para de esperar e o comando roda. Ambos transformam uma regra que está correta para cada comando em uma regra que pode ser desligada pelo formato da entrada. Um agente que segue uma prompt injection pode produzir uma entrada assim sem que ninguém a escreva à mão.
Qual é o impacto?
Um agente que executasse um desses comandos, por exemplo depois de ler uma prompt injection, podia imprimir um arquivo protegido no seu contexto. Esse é o cenário que o hook existe para impedir. A pontuação CVSS 3.1 é 5,5, média, porque exige um agente local e uma ação do usuário, e o impacto na confidencialidade é alto enquanto a integridade e a disponibilidade não são afetadas.
Dois fatos o limitam. Os arquivos que já tinham shadow com zn env shadow não ficaram expostos pelos caminhos do .env, porque contêm apenas marcadores. Outros arquivos de credenciais (~/.aws/credentials, ~/.ssh/ e semelhantes) ficaram expostos apenas pelas formas de cadeia longa e de timeout. O scrubber de saída, uma camada separada, continua ocultando formatos conhecidos de segredo na saída das ferramentas, o que reduziu o que chegava ao modelo para esses formatos. Ele é uma segunda linha, não um substituto da proteção: um segredo em um formato que ele não conhece passa.
Como a 1.12.0 fecha esses caminhos?
A correção tem quatro partes.
1. Resolver antes de decidir. A proteção agora calcula o que o shell calcularia, até onde o texto permite, e verifica o resultado. Os padrões são expandidos em relação à pasta em que o comando de fato estará. Uma mudança de diretório atualiza a base para os comandos seguintes. Os caminhos montados a partir de variáveis e substituições são resolvidos onde for possível, e o código entregue a um interpretador ou a outro shell (bash -c "cat app/.env", ou um script passado a um runtime de linguagem) é lido como um comando por si só. Várias grafias da mesma string e várias formas de redirecionamento são normalizadas para o arquivo que abrem. Uma leitura de um arquivo protegido a partir do repositório git ou do seu histórico é recusada como uma leitura do disco, mesmo quando o arquivo não está mais no disco. Os comandos de limpeza de que as pessoas precisam continuam funcionando: git rm --cached .env, git restore --staged .env, git status e git check-ignore .env.
2. Verificar todos os comandos. As versões anteriores olhavam apenas os primeiros 1000 comandos simples de uma cadeia, contando também as linhas, e deixavam o resto rodar. A 1.12.0 verifica todos os comandos de uma cadeia.
3. Recusar o que não pode ser verificado a tempo. A verificação roda dentro de um orçamento de tempo e de disco bem abaixo do timeout do hook do agente. Um comando com caminhos demais para resolver dentro desse orçamento é recusado. O agente recebe uma resposta antes de desistir de esperar, e a resposta é não. Dois detalhes sustentam isso: um FIFO ou outro arquivo especial com um nome protegido é recusado sem ser lido, e um nome repetido muitas vezes em um comando longo é verificado uma única vez.
4. Arquivos com shadow como defesa em profundidade. Ler um caminho corretamente é uma previsão sobre um shell. O zn env shadow elimina a dependência dessa previsão para os arquivos que cobre: o arquivo no disco contém marcadores como __ZN_API_KEY__, e o zn só devolve os valores reais nos comandos que você permite. Se um caminho futuro escapar da proteção, ele encontrará marcadores.
A release também acrescenta proteção para chaves privadas guardadas fora de ~/.ssh, por exemplo em um projeto: nomes de chaves SSH, repositórios de chaves (.p12, .pfx, .jks, .ppk) e arquivos .pem, .key ou .p8 que contenham uma chave privada. Certificados e chaves públicas continuam legíveis.
Cada caminho tem um teste de regressão na 1.12.0, e todos passam. Usamos os nomes exatos das classes do aviso e nada além deles neste artigo, de propósito. Se você encontrar outro caminho, escreva para security@usezn.com.
O que diríamos a quem escreve uma proteção para agentes?
Estas são as lições que tiramos, ordenadas pelo quanto mudaram o design.
- Decida pelo que o comando vai fazer, não pelo que ele diz. Cada expansão que o shell faz antes de executar é um lugar em que texto e significado diferem. Liste-as e, para cada uma, decida se você a resolve ou a recusa.
- Recuse o que você não consegue resolver. Uma variável não resolvida em um caminho que poderia apontar para um arquivo protegido não é motivo para permitir. É motivo para pedir ao agente um comando mais simples.
- Verifique toda a entrada. Uma proteção que examina um prefixo tem uma fronteira, e a fronteira é um lugar para colocar o ataque. Não ponha limite no número de comandos que você verifica, ou faça com que ultrapassar o limite seja uma recusa.
- Trate o tempo como parte da política. Descubra quanto tempo o agente hospedeiro espera pelo seu hook, defina seu próprio orçamento bem abaixo disso e faça um timeout significar não. Uma proteção que falha aberta sob carga só protege quando ninguém está pressionando.
- Um arquivo tem várias moradas. Disco, índice, histórico, backups, caches de build. Se a regra é sobre o conteúdo, pense em cada lugar de onde o conteúdo pode ser lido.
- Adicione uma camada que não dependa do parser. Interpretar o shell é difícil e vai continuar tendo brechas. Fazer o segredo estar ausente do arquivo, como o shadow faz, ou ocultar formatos conhecidos na saída, continua funcionando quando o parser erra.
- Escreva o teste de regressão para a classe, não só para o exemplo. O primeiro teste é para o comando que foi relatado. O útil pergunta o que mais compartilha a causa dele.
O que você deve fazer agora?
-
Atualize os pacotes:
npm install -g zn-gate@1.12.0 pip install -U zn-gate -
Reinstale os hooks dos agentes:
npx -y zn-gate@1.12.0 initSe você usa o plugin do Claude Code, atualize-o pelo diretório de plugins assim que a 1.12.0 aparecer lá.
-
Em cada pasta que tenha um arquivo
.envcom valores, aplique o shadow:zn env shadow .env -
Verifique quais agentes têm os hooks instalados:
zn-gate check
Se você não puder atualizar agora, o passo 3 cobre os caminhos do .env. As formas de cadeia longa e de timeout não têm solução alternativa nas versões antigas.
O que isto não cobre?
- Uma verificação estática sobre texto de shell sempre terá limites. Alguns comandos não podem ser resolvidos sem executá-los, e para esses a escolha é recusar. Isso pode recusar um comando inofensivo, e o agente precisa reformulá-lo.
- A proteção cobre as leituras que passam pelos hooks de shell do agente. Ela não controla o que um processo que você mesmo iniciou faz.
- O scrubber de saída oculta formatos conhecidos de segredo. Um segredo em um formato que ele não conhece pode passar por ele.
- Este artigo não lista todas as grafias de todas as formas. Isso é deliberado enquanto os usuários atualizam.
- Uma proteção sobre texto de shell é uma barreira de segurança, não um sandbox: ela cobre o que as ferramentas normais do agente fazem, e um processo determinado pode achar outro jeito de ler um arquivo. Um sandbox de agente que corta o acesso à rede é uma segunda camada útil.
- Atualizar não recolhe nada. Um segredo que chegou ao contexto do modelo antes de você atualizar fica lá durante aquela sessão, então, se um valor pode ter sido impresso antes, troque-o.
Perguntas frequentes
Quais versões do zn-gate podiam deixar um agente ler um arquivo protegido de forma indireta?
O zn-gate 1.5.0 a 1.11.0. O problema é o GHSA-rccq-h3xc-j9qj, de gravidade média, e está corrigido na 1.12.0.
O que preciso fazer?
Atualize com npm install -g zn-gate@1.12.0 e pip install -U zn-gate, depois execute npx -y zn-gate@1.12.0 init para reinstalar os hooks. Em cada pasta com um arquivo .env que tenha valores, execute zn env shadow .env. Execute zn-gate check para ver quais agentes têm os hooks instalados.
Os arquivos protegidos com zn env shadow ficaram expostos?
Não pelos caminhos do .env. Um arquivo com shadow contém apenas marcadores no formato __ZN_NAME__, então lê-lo por qualquer caminho não revela nenhum valor. Outros arquivos de credenciais, como ~/.aws/credentials e ~/.ssh/, ficaram expostos apenas pelas formas de cadeia longa e de timeout.
Existe uma solução alternativa para versões anteriores à 1.12.0?
Para os caminhos do .env, execute zn env shadow .env em cada pasta com um arquivo .env que tenha valores. As formas de cadeia longa e de timeout não têm solução alternativa nas versões antigas, então atualizar é a única correção para elas.
Por que o zn-gate recusa um comando que não consegue verificar a tempo, em vez de deixá-lo rodar?
Porque uma proteção que permite o que não conseguiu verificar dá a um atacante um jeito de desligá-la: basta tornar o comando lento ou longo. Recusar custa uma nova tentativa do agente. Permitir custa a proteção.
Onde posso ler o aviso?
O GHSA-rccq-h3xc-j9qj no GitHub. A prova de conceito completa não é publicada enquanto os usuários atualizam.
As correções estão no zn-gate 1.12.0 (MIT, local, sem conta), e o resto da release está em novidades do zn-gate 1.12.0. Para o básico sobre como manter um .env fora do alcance de um agente, consulte como impedir com um só comando que o Claude Code, o Codex CLI, o Cursor e o OpenCode leiam seu .env. O código está no GitHub, e Instalação cobre cada agente.