Comment un agent de programmation pouvait-il lire votre fichier .env de façon indirecte, et comment zn-gate 1.12.0 a-t-il fermé ces chemins ?

IngénierieJohn Silver14 min de lecture

Pour zn-gate 1.5.0 à 1.11.0, la réponse est oui, de quelques façons précises. Les hooks des agents refusaient la commande évidente qui lit un fichier .env, mais ils jugeaient le chemin tel qu'il était écrit dans la commande, relatif au dossier courant. Une commande qui atteignait le même fichier par un autre chemin pouvait s'exécuter. La version 1.12.0 résout ces formes avant de décider, vérifie chaque commande d'une chaîne et refuse ce qu'elle ne peut pas vérifier dans une limite de temps. Le problème est GHSA-rccq-h3xc-j9qj, de gravité moyenne. L'équipe zn l'a trouvé lors d'une revue interne.

Cet article décrit les classes de chemins au même niveau que l'avis publié, la conception du correctif et ce que nous dirions à quiconque écrit une protection pour agents de programmation. Les seules commandes citées sont les exemples déjà publiés dans le changelog de la 1.12.0 ; il n'ajoute aucune forme nouvelle ni preuve de concept. La preuve de concept complète reste non publiée pendant que les utilisateurs mettent à jour.

Que fait la protection en lecture de zn-gate ?

zn-gate installe des hooks dans Claude Code, Codex CLI, Cursor et OpenCode. Avant que l'agent exécute une commande de shell, le hook reçoit le texte de la commande et répond autoriser ou refuser. L'une de ses tâches est de refuser les commandes qui liraient un fichier protégé, afin que son contenu n'atteigne pas le modèle. Les fichiers protégés sont les fichiers .env qui contiennent des valeurs en clair et les fichiers d'identifiants comme ~/.aws/credentials.

La protection n'exécute pas la commande. Elle lit le texte, trouve les noms de fichiers qu'il mentionne et décide. C'est une vérification statique sur un langage qui n'est pas statique : le shell développe, substitue et réinterprète du texte pendant l'exécution. L'écart entre ce que voit la protection et ce que fait le shell est l'origine de chacun des chemins ci-dessous.

Un fichier .env dont zn env shadow a remplacé les valeurs par des marqueurs n'est pas une cible protégée de la même manière. Il contient des noms et des marqueurs de la forme __ZN_<NAME>__, donc le lire par n'importe quel chemin ne montre aucune valeur. C'est pourquoi le masquage compte pour la suite.

Quels chemins la protection laissait-elle passer ?

L'avis décrit ces formes. Les voici sous forme de classes.

Classe Ce qui n'allait pas Exemple du changelog public
Jokers Un motif dans un chemin n'était pas développé avant la vérification : la protection cherchait un fichier portant le nom du motif et ne trouvait rien cat app*/.env
Changement de répertoire La vérification résolvait les chemins depuis le dossier où s'exécute le hook, et non depuis le dossier où la commande s'était déplacée cd app && cat .env
Chemins construits pendant l'exécution de la commande Une variable ou une substitution de commande fournissait une partie du chemin, et sa valeur n'existait pas au moment de la vérification un chemin qui commence par une variable ou une substitution de commande
Certaines formes de guillemets et de redirection du shell Le shell a plusieurs manières d'écrire la même chaîne et d'ouvrir un fichier en lecture, et la protection ne comprenait que la plus simple
La copie stockée dans git Un .env validé une fois, ou conservé dans l'index, peut être affiché depuis le dépôt même s'il n'est plus sur le disque git show HEAD:.env
Chaînes très longues, vérifiées en partie seulement Seules les premières commandes d'une longue chaîne étaient examinées : une lecture placée à la fin n'était pas vérifiée, pour aucun fichier protégé
Une vérification plus lente que le délai du hook Chaque agent n'attend le hook qu'un temps limité ; une commande dont la vérification prenait plus de temps s'exécutait sans contrôle

Les cinq premières sont des façons de nommer un fichier que la protection ne résolvait pas. Les deux dernières sont d'une autre nature : elles ne cachent pas le fichier, elles font que la vérification n'a pas lieu. Les formes de longue chaîne et de délai dépassé s'appliquaient à tous les fichiers protégés, y compris les fichiers d'identifiants hors de .env.

Pourquoi un joker pose-t-il problème à une vérification de chemin ?

Parce que la protection et le shell ne s'accordent pas sur le sens du texte. Le shell transforme un motif en la liste des chemins correspondants avant que la commande démarre. Une protection qui traite le motif comme un nom littéral demande si un fichier portant exactement ce nom existe, et la réponse est non. La commande est alors autorisée, et le shell la développe en un vrai fichier. Dans la 1.11.0, le chemin simple était refusé et le même fichier atteint par un motif dans le nom d'un dossier ne l'était pas.

Pourquoi le changement de répertoire compte-t-il ?

Les chemins d'une commande ont un sens relatif au dossier courant, et une commande peut le changer en cours de route. La protection résolvait ce qu'elle lisait par rapport au dossier où l'agent avait démarré. Dès qu'une commande se déplace, cette base est fausse. Il en va de même pour un .env déjà masqué dans le dossier de départ : la protection pouvait se contenter du fichier masqué alors que la commande en lisait un autre.

Qu'y a-t-il de problématique avec les chemins construits pendant l'exécution ?

Leur valeur n'existe pas quand la protection regarde. Une variable définie plus tôt dans la chaîne, ou la sortie d'une substitution de commande, ne devient du texte que lorsque le shell l'exécute. La protection voit un espace réservé et ne peut pas savoir ce qu'il deviendra.

Et les guillemets, la redirection et la copie git ?

Les shells acceptent de nombreuses graphies pour la même chaîne, et plusieurs façons d'ouvrir un fichier en lecture en plus de le nommer comme argument. Une protection qui ne normalise que la graphie courante manquera les autres. Le chemin git est une observation différente : un fichier protégé a plusieurs endroits où vivre. La copie de l'arbre de travail est celle à laquelle tout le monde pense. La copie validée et l'historique peuvent être affichés avec des commandes git, et ils sont toujours là après la suppression ou l'exclusion du fichier.

Pourquoi les longues chaînes et les vérifications lentes comptent-elles plus que les autres ?

Parce qu'elles ne dépendent pas de la façon dont le fichier est nommé. Si la protection n'examine que les premières commandes d'une longue chaîne, tout ce qui est placé après ce point est hors de la vérification. Si la protection a besoin de plus de temps que l'agent n'en attend, l'agent cesse d'attendre et la commande s'exécute. Les deux transforment une règle correcte pour chaque commande en une règle que la forme de l'entrée peut désactiver. Un agent qui suit une injection de prompt peut produire une telle entrée sans que personne l'écrive à la main.

Quel est l'impact ?

Un agent qui exécutait l'une de ces commandes, par exemple après avoir lu une injection de prompt, pouvait afficher un fichier protégé dans son contexte. C'est le scénario que le hook existe pour empêcher. Le score CVSS 3.1 est de 5,5, moyen, car il faut un agent local et une action de l'utilisateur, et l'impact sur la confidentialité est élevé alors que l'intégrité et la disponibilité ne sont pas touchées.

Deux faits le limitent. Les fichiers déjà masqués avec zn env shadow n'étaient pas exposés par les chemins liés à .env, car ils ne contiennent que des marqueurs. Les autres fichiers d'identifiants (~/.aws/credentials, ~/.ssh/ et similaires) n'ont été exposés que par les formes de longue chaîne et de délai dépassé. Le nettoyeur de sortie, une couche distincte, continuait à masquer les formats de secrets connus dans la sortie des outils, ce qui a réduit ce qui atteignait le modèle pour ces formats. C'est une seconde ligne, pas un remplacement de la protection : un secret dans un format qu'il ne connaît pas passe.

Comment la 1.12.0 ferme-t-elle ces chemins ?

Le correctif comporte quatre parties.

1. Résoudre avant de décider. La protection calcule désormais ce que calculerait le shell, autant que le texte le permet, et vérifie le résultat. Les motifs sont développés par rapport au dossier dans lequel la commande se trouvera réellement. Un changement de répertoire met à jour la base pour les commandes suivantes. Les chemins construits à partir de variables et de substitutions sont résolus lorsque c'est possible, et le code confié à un interpréteur ou à un autre shell (bash -c "cat app/.env", ou un script passé à un environnement d'exécution de langage) est lu comme une commande à part entière. Plusieurs graphies d'une même chaîne et plusieurs formes de redirection sont normalisées vers le fichier qu'elles ouvrent. La lecture d'un fichier protégé depuis le dépôt git ou son historique est refusée comme une lecture depuis le disque, même lorsque le fichier n'est plus sur le disque. Les commandes de nettoyage dont on a besoin fonctionnent toujours : git rm --cached .env, git restore --staged .env, git status et git check-ignore .env.

2. Vérifier chaque commande. Les versions précédentes ne regardaient que les 1000 premières commandes simples d'une chaîne, lignes comprises, et laissaient le reste s'exécuter. La 1.12.0 vérifie chaque commande d'une chaîne.

3. Refuser ce qui ne peut pas être vérifié à temps. La vérification s'exécute dans un budget de temps et de disque bien inférieur au délai du hook de l'agent. Une commande comportant trop de chemins à résoudre dans ce budget est refusée. L'agent reçoit une réponse avant de cesser d'attendre, et la réponse est non. Deux détails y contribuent : un FIFO ou un autre fichier spécial portant un nom protégé est refusé sans être lu, et un nom répété de nombreuses fois dans une longue commande n'est vérifié qu'une fois.

4. Fichiers masqués comme défense en profondeur. Lire correctement un chemin est une prédiction sur un shell. zn env shadow supprime la dépendance à cette prédiction pour les fichiers qu'il couvre : le fichier sur le disque contient des marqueurs comme __ZN_API_KEY__, et zn ne remet les vraies valeurs que dans les commandes que vous autorisez. Si un futur chemin échappe à la protection, il ne trouvera que des marqueurs.

La version ajoute aussi une protection pour les clés privées conservées hors de ~/.ssh, par exemple dans un projet : noms de clés SSH, magasins de clés (.p12, .pfx, .jks, .ppk) et fichiers .pem, .key ou .p8 qui contiennent une clé privée. Les certificats et les clés publiques restent lisibles.

Chaque chemin dispose d'un test de régression dans la 1.12.0, et ils passent tous. Nous avons volontairement utilisé dans cet article les noms exacts des classes de l'avis, et rien de plus. Si vous trouvez un autre chemin, écrivez à security@usezn.com.

Que dirions-nous à quelqu'un qui écrit une protection pour agents ?

Voici les leçons que nous en avons tirées, classées selon l'ampleur de leur effet sur la conception.

  1. Décidez d'après ce que fera la commande, pas d'après ce qu'elle dit. Chaque expansion que le shell effectue avant d'exécuter est un endroit où le texte et le sens diffèrent. Dressez-en la liste et, pour chacune, décidez si vous la résolvez ou la refusez.
  2. Refusez ce que vous ne pouvez pas résoudre. Une variable non résolue dans un chemin qui pourrait désigner un fichier protégé n'est pas une raison d'autoriser. C'est une raison de demander à l'agent une commande plus simple.
  3. Vérifiez toute l'entrée. Une protection qui n'examine qu'un préfixe a une frontière, et la frontière est un endroit où placer l'attaque. Ne fixez aucune limite au nombre de commandes vérifiées, ou faites du dépassement de la limite un refus.
  4. Traitez le temps comme une partie de la politique. Trouvez combien de temps l'agent hôte attend votre hook, fixez votre propre budget bien en dessous et faites d'un délai dépassé un non. Une protection qui s'ouvre en cas de charge ne protège que lorsque personne n'insiste.
  5. Un fichier a plusieurs domiciles. Disque, index, historique, sauvegardes, caches de compilation. Si la règle concerne le contenu, pensez à chaque endroit d'où le contenu peut être lu.
  6. Ajoutez une couche qui ne dépend pas de l'analyseur. Analyser le shell est difficile et gardera des lacunes. Faire en sorte que le secret soit absent du fichier, comme le fait le masquage, ou masquer les formats connus en sortie, continue de fonctionner quand l'analyseur se trompe.
  7. Écrivez le test de régression pour la classe, pas seulement pour l'exemple. Le premier test porte sur la commande signalée. Le test utile demande ce qui partage sa cause.

Que devriez-vous faire maintenant ?

  1. Mettez à jour les paquets :

    npm install -g zn-gate@1.12.0
    pip install -U zn-gate
  2. Réinstallez les hooks des agents :

    npx -y zn-gate@1.12.0 init

    Si vous utilisez le plugin Claude Code, mettez-le à jour depuis le répertoire des plugins dès que la 1.12.0 y figure.

  3. Dans chaque dossier qui contient un fichier .env avec des valeurs, masquez-le :

    zn env shadow .env
  4. Vérifiez quels agents ont les hooks installés :

    zn-gate check

Si vous ne pouvez pas mettre à jour tout de suite, l'étape 3 couvre les chemins liés à .env. Les formes de longue chaîne et de délai dépassé n'ont pas de contournement dans les versions plus anciennes.

Qu'est-ce que cela ne couvre pas ?

  • Une vérification statique sur du texte de shell aura toujours des limites. Certaines commandes ne peuvent pas être résolues sans être exécutées et, pour celles-là, le choix est de refuser. Cela peut refuser une commande inoffensive, et l'agent doit la reformuler.
  • La protection couvre les lectures qui passent par les hooks de shell de l'agent. Elle ne contrôle pas ce que fait un processus que vous avez lancé vous-même.
  • Le nettoyeur de sortie masque les formats de secrets connus. Un secret dans un format qu'il ne connaît pas peut passer.
  • Cet article ne liste pas toutes les graphies de toutes les formes. C'est délibéré pendant que les utilisateurs mettent à jour.
  • Une protection sur du texte de shell est un garde-fou, pas un bac à sable : elle couvre ce que font les outils normaux de l'agent, et un processus déterminé peut trouver une autre façon de lire un fichier. Un bac à sable de l'agent qui coupe l'accès au réseau est une deuxième couche utile.
  • Mettre à jour ne rappelle rien. Un secret arrivé dans le contexte du modèle avant la mise à jour y reste pour cette session ; si une valeur a pu être affichée plus tôt, faites-la tourner.

Questions fréquentes

Quelles versions de zn-gate pouvaient laisser un agent lire un fichier protégé de façon indirecte ?

zn-gate 1.5.0 à 1.11.0. Le problème est GHSA-rccq-h3xc-j9qj, de gravité moyenne, et il est corrigé dans la 1.12.0.

Que dois-je faire ?

Mettez à jour avec npm install -g zn-gate@1.12.0 et pip install -U zn-gate, puis exécutez npx -y zn-gate@1.12.0 init pour réinstaller les hooks. Dans chaque dossier contenant un fichier .env avec des valeurs, exécutez zn env shadow .env. Exécutez zn-gate check pour voir quels agents ont les hooks installés.

Les fichiers protégés avec zn env shadow étaient-ils exposés ?

Pas par les chemins liés à .env. Un fichier masqué ne contient que des marqueurs de la forme __ZN_NAME__, donc le lire par n'importe quel chemin ne révèle aucune valeur. Les autres fichiers d'identifiants, comme ~/.aws/credentials et ~/.ssh/, n'ont été exposés que par les formes de longue chaîne et de délai dépassé.

Existe-t-il un contournement pour les versions antérieures à la 1.12.0 ?

Pour les chemins liés à .env, exécutez zn env shadow .env dans chaque dossier contenant un fichier .env avec des valeurs. Les formes de longue chaîne et de délai dépassé n'ont pas de contournement dans les versions plus anciennes : la mise à jour est donc le seul correctif pour elles.

Pourquoi zn-gate refuse-t-il une commande qu'il ne peut pas vérifier à temps au lieu de la laisser s'exécuter ?

Une protection qui autorise ce qu'elle n'a pas pu vérifier donne à l'attaquant un moyen de la désactiver : rendre la commande lente ou longue. Refuser coûte un nouvel essai de l'agent. Autoriser coûte la protection.

Où puis-je lire l'avis ?

GHSA-rccq-h3xc-j9qj sur GitHub. La preuve de concept complète n'est pas publiée pendant que les utilisateurs mettent à jour.


Les correctifs sont dans zn-gate 1.12.0 (MIT, local, sans compte), et le reste de la version est dans nouveautés de zn-gate 1.12.0. Pour les bases sur la façon de tenir un .env à l'écart d'un agent, consultez comment empêcher Claude Code, Codex CLI, Cursor et OpenCode de lire votre .env avec une seule commande. Le code est sur GitHub, et Installation couvre chaque agent.

Partager