¿Cómo podía un agente de programación leer tu archivo .env de forma indirecta y cómo cerró esas rutas zn-gate 1.12.0?
Para zn-gate 1.5.0 a 1.11.0, la respuesta es sí, de unas pocas formas concretas. Los hooks de los agentes rechazaban el comando evidente que lee un archivo .env, pero juzgaban la ruta tal como estaba escrita en el comando, relativa a la carpeta actual. Un comando que llegaba al mismo archivo por otra vía podía ejecutarse. La versión 1.12.0 resuelve esas formas antes de decidir, comprueba todos los comandos de una cadena y rechaza lo que no puede comprobar dentro de un límite de tiempo. El problema es GHSA-rccq-h3xc-j9qj, de gravedad media. El equipo de zn lo encontró en una revisión interna.
Este artículo describe las clases de rutas al mismo nivel que el aviso publicado, cómo está diseñada la corrección y qué le diríamos a cualquiera que escriba una protección para agentes de programación. Los únicos comandos que cita son los ejemplos ya publicados en el changelog de la 1.12.0; no añade ninguna forma nueva ni prueba de concepto. La prueba de concepto completa sigue sin publicarse mientras los usuarios actualizan.
¿Qué hace la protección de lectura de zn-gate?
zn-gate instala hooks en Claude Code, Codex CLI, Cursor y OpenCode. Antes de que el agente ejecute un comando de shell, el hook recibe el texto del comando y responde permitir o rechazar. Una de sus tareas es rechazar los comandos que leerían un archivo protegido, para que su contenido no llegue al modelo. Los archivos protegidos son los .env que contienen valores en texto plano y los archivos de credenciales como ~/.aws/credentials.
La protección no ejecuta el comando. Lee el texto, encuentra los nombres de archivo que menciona y decide. Es una comprobación estática sobre un lenguaje que no es estático: el shell expande, sustituye y reinterpreta texto mientras se ejecuta. La distancia entre lo que ve la protección y lo que hace el shell es de donde sale cada una de las rutas siguientes.
Un archivo .env cuyos valores zn env shadow ha sustituido por marcadores no es un objetivo protegido de la misma manera. Contiene nombres y marcadores con la forma __ZN_<NAME>__, así que leerlo por cualquier ruta no muestra ningún valor. Por eso importa el uso de las sombras en lo que sigue.
¿Qué rutas dejaba pasar la protección?
El aviso describe estas formas. Aquí están como clases.
| Clase | Qué fallaba | Ejemplo del changelog público |
|---|---|---|
| Comodines | Un patrón en una ruta no se expandía antes de la comprobación, así que la protección buscaba un archivo con el nombre del patrón y no encontraba nada | cat app*/.env |
| Cambio de directorio | La comprobación resolvía las rutas desde la carpeta donde se ejecuta el hook, no desde la carpeta a la que se había movido el comando | cd app && cat .env |
| Rutas construidas mientras se ejecuta el comando | Una variable o una sustitución de comandos aportaba parte de la ruta, y su valor no existía en el momento de la comprobación | una ruta que empieza por una variable o una sustitución de comandos |
| Algunas formas de entrecomillado y redirección del shell | El shell tiene varias maneras de escribir la misma cadena y de abrir un archivo para leerlo, y la protección solo entendía la más simple | |
| La copia guardada en git | Un .env confirmado una vez, o guardado en el índice, puede imprimirse desde el repositorio aunque ya no esté en disco |
git show HEAD:.env |
| Cadenas muy largas, comprobadas solo en parte | Solo se examinaban los primeros comandos de una cadena larga, así que una lectura al final no se comprobaba, para ningún archivo protegido | |
| Una comprobación más lenta que el límite de tiempo del hook | Cada agente espera un tiempo limitado al hook; un comando que tardaba más en comprobarse que eso se ejecutaba sin comprobar |
Las cinco primeras son formas de nombrar un archivo que la protección no resolvía. Las dos últimas son distintas por naturaleza: no ocultan el archivo, hacen que la comprobación no ocurra. Las formas de cadena larga y de tiempo de espera se aplicaban a todos los archivos protegidos, incluidos los archivos de credenciales fuera de .env.
¿Por qué un comodín es un problema para una comprobación de rutas?
Porque la protección y el shell no coinciden en lo que significa el texto. El shell convierte un patrón en la lista de rutas que coinciden antes de que empiece el comando. Una protección que trata el patrón como un nombre literal pregunta si existe un archivo con exactamente ese nombre, y la respuesta es no. El comando se permite y el shell lo expande a un archivo real. En la 1.11.0 se rechazaba la ruta simple y el mismo archivo alcanzado mediante un patrón en el nombre de una carpeta no.
¿Por qué importa cambiar de directorio?
Las rutas de un comando significan algo relativo a la carpeta actual, y un comando puede cambiarla a mitad. La protección resolvía lo que leía contra la carpeta donde empezó el agente. Cuando un comando se mueve, esa base es incorrecta. Lo mismo ocurre con un .env que ya está en sombra en la carpeta de partida: la protección podía darse por satisfecha con el archivo en sombra mientras el comando leía otro distinto.
¿Qué pasa con las rutas construidas mientras se ejecuta el comando?
Su valor no existe cuando la protección mira. Una variable definida antes en la cadena, o la salida de una sustitución de comandos, solo se convierte en texto cuando el shell la ejecuta. La protección ve un marcador de posición y no puede saber en qué se convertirá.
¿Y el entrecomillado, la redirección y la copia de git?
Los shells aceptan muchas grafías para la misma cadena y varias maneras de abrir un archivo para leerlo además de nombrarlo como argumento. Una protección que normaliza solo la grafía común se perderá las demás. La ruta de git es una observación distinta: un archivo protegido tiene más de un lugar donde vivir. La copia del árbol de trabajo es en la que todos piensan. La copia confirmada y el historial se pueden imprimir con comandos de git, y siguen ahí después de borrar o ignorar el archivo.
¿Por qué importan más las cadenas largas y las comprobaciones lentas que las demás?
Porque no dependen de cómo se nombra el archivo. Si la protección examina solo los primeros comandos de una cadena larga, todo lo que se coloque después de ese punto queda fuera de la comprobación. Si la protección necesita más tiempo del que espera el agente, el agente deja de esperar y el comando se ejecuta. Ambas convierten una regla correcta para cada comando en una regla que se puede apagar con la forma de la entrada. Un agente que sigue una inyección de prompt puede producir una entrada así sin que nadie la escriba a mano.
¿Cuál es el impacto?
Un agente que ejecutara uno de estos comandos, por ejemplo tras leer una inyección de prompt, podía imprimir un archivo protegido en su contexto. Es el escenario que el hook existe para evitar. La puntuación CVSS 3.1 es 5,5, media, porque requiere un agente local y una acción del usuario, y el impacto en la confidencialidad es alto mientras que la integridad y la disponibilidad no se ven afectadas.
Dos hechos lo limitan. Los archivos que ya estaban en sombra con zn env shadow no estaban expuestos por las rutas de .env, porque solo contienen marcadores. Otros archivos de credenciales (~/.aws/credentials, ~/.ssh/ y similares) solo estuvieron expuestos por las formas de cadena larga y de tiempo de espera. El depurador de salida, una capa aparte, seguía ocultando los formatos de secretos conocidos en la salida de las herramientas, lo que redujo lo que llegaba al modelo para esos formatos. Es una segunda línea, no un sustituto de la protección: un secreto en un formato que no conoce pasa.
¿Cómo cierra la 1.12.0 estas rutas?
La corrección tiene cuatro partes.
1. Resolver antes de decidir. La protección calcula ahora lo que calcularía el shell, hasta donde el texto lo permite, y comprueba el resultado. Los patrones se expanden respecto a la carpeta en la que realmente estará el comando. Un cambio de directorio actualiza la base para los comandos siguientes. Las rutas construidas con variables y sustituciones se resuelven donde se puede, y el código que se entrega a un intérprete o a otro shell (bash -c "cat app/.env", o un script pasado a un entorno de ejecución de un lenguaje) se lee como un comando por derecho propio. Varias grafías de la misma cadena y varias formas de redirección se normalizan al archivo que abren. La lectura de un archivo protegido desde el repositorio git o su historial se rechaza igual que una lectura desde el disco, también cuando el archivo ya no está en disco. Los comandos de limpieza que la gente necesita siguen funcionando: git rm --cached .env, git restore --staged .env, git status y git check-ignore .env.
2. Comprobar todos los comandos. Las versiones anteriores solo miraban los primeros 1000 comandos simples de una cadena, contando también las líneas, y dejaban ejecutar el resto. La 1.12.0 comprueba todos los comandos de una cadena.
3. Rechazar lo que no se puede comprobar a tiempo. La comprobación se ejecuta dentro de un presupuesto de tiempo y de disco muy inferior al límite de tiempo del hook del agente. Un comando con demasiadas rutas que resolver dentro de ese presupuesto se rechaza. El agente recibe una respuesta antes de dejar de esperar, y la respuesta es no. Dos detalles lo apoyan: un FIFO u otro archivo especial con un nombre protegido se rechaza sin leerlo, y un nombre repetido muchas veces en un comando largo se comprueba una sola vez.
4. Archivos en sombra como defensa en profundidad. Leer bien una ruta es una predicción sobre un shell. zn env shadow elimina la dependencia de esa predicción para los archivos que cubre: el archivo en disco contiene marcadores como __ZN_API_KEY__, y zn vuelve a poner los valores reales solo en los comandos que permites. Si una ruta futura se escapa de la protección, encontrará marcadores.
La versión también añade protección para las claves privadas guardadas fuera de ~/.ssh, por ejemplo en un proyecto: nombres de claves SSH, almacenes de claves (.p12, .pfx, .jks, .ppk) y archivos .pem, .key o .p8 que contienen una clave privada. Los certificados y las claves públicas siguen siendo legibles.
Cada ruta tiene una prueba de regresión en la 1.12.0, y todas pasan. En este artículo hemos usado a propósito los nombres exactos de las clases del aviso y nada más. Si encuentras otra ruta, escribe a security@usezn.com.
¿Qué le diríamos a quien escriba una protección para agentes?
Estas son las lecciones que sacamos, ordenadas por cuánto cambiaron el diseño.
- Decide según lo que hará el comando, no según lo que dice. Cada expansión que hace el shell antes de ejecutar es un lugar donde el texto y el significado difieren. Haz la lista y, para cada una, decide si la resuelves o la rechazas.
- Rechaza lo que no puedas resolver. Una variable sin resolver en una ruta que podría apuntar a un archivo protegido no es un motivo para permitir. Es un motivo para pedirle al agente un comando más simple.
- Comprueba toda la entrada. Una protección que examina un prefijo tiene un límite, y el límite es un sitio donde colocar el ataque. No pongas tope al número de comandos que compruebas, o haz que superar el tope sea un rechazo.
- Trata el tiempo como parte de la política. Averigua cuánto espera el agente anfitrión a tu hook, fija un presupuesto propio muy por debajo y haz que un tiempo agotado signifique no. Una protección que falla abierta bajo carga solo protege cuando nadie la presiona.
- Un archivo tiene varios hogares. Disco, índice, historial, copias de seguridad, cachés de compilación. Si la regla trata del contenido, piensa en cada lugar desde el que se puede leer.
- Añade una capa que no dependa del analizador. Analizar el shell es difícil y seguirá teniendo huecos. Hacer que el secreto no esté en el archivo, como hacen las sombras, o ocultar formatos conocidos a la salida, sigue funcionando cuando el analizador se equivoca.
- Escribe la prueba de regresión para la clase, no solo para el ejemplo. La primera prueba es para el comando que se notificó. La útil pregunta qué más comparte su causa.
¿Qué deberías hacer ahora?
-
Actualiza los paquetes:
npm install -g zn-gate@1.12.0 pip install -U zn-gate -
Reinstala los hooks de los agentes:
npx -y zn-gate@1.12.0 initSi usas el plugin de Claude Code, actualízalo desde el directorio de plugins cuando aparezca la 1.12.0.
-
En cada carpeta que tenga un archivo
.envcon valores, ponlo en sombra:zn env shadow .env -
Comprueba qué agentes tienen los hooks instalados:
zn-gate check
Si no puedes actualizar ahora mismo, el paso 3 cubre las rutas de .env. Las formas de cadena larga y de tiempo de espera no tienen solución provisional en las versiones anteriores.
¿Qué no cubre esto?
- Una comprobación estática sobre texto de shell siempre tendrá límites. Algunos comandos no se pueden resolver sin ejecutarlos y, para ellos, la opción es rechazar. Eso puede rechazar un comando inofensivo, y el agente tiene que reformularlo.
- La protección cubre las lecturas que pasan por los hooks de shell del agente. No controla lo que hace un proceso que lanzaste tú mismo.
- El depurador de salida oculta los formatos de secretos conocidos. Un secreto en un formato que no conoce puede pasar.
- Este artículo no lista todas las grafías de todas las formas. Es deliberado mientras los usuarios actualizan.
- Una protección sobre texto de shell es una barandilla, no un sandbox: cubre lo que hacen las herramientas normales del agente, y un proceso decidido puede encontrar otra forma de leer un archivo. Un sandbox del agente que corte el acceso a la red es una segunda capa útil.
- Actualizar no retira nada. Un secreto que llegó al contexto del modelo antes de actualizar sigue ahí durante esa sesión, así que, si un valor pudo imprimirse antes, rótalo.
Preguntas frecuentes
¿Qué versiones de zn-gate podían dejar que un agente leyera un archivo protegido de forma indirecta?
De zn-gate 1.5.0 a 1.11.0. El problema es GHSA-rccq-h3xc-j9qj, de gravedad media, y está corregido en la 1.12.0.
¿Qué tengo que hacer?
Actualiza con npm install -g zn-gate@1.12.0 y pip install -U zn-gate, y ejecuta npx -y zn-gate@1.12.0 init para reinstalar los hooks. En cada carpeta con un archivo .env que tenga valores, ejecuta zn env shadow .env. Ejecuta zn-gate check para ver qué agentes tienen los hooks instalados.
¿Estaban expuestos los archivos protegidos con zn env shadow?
No a través de las rutas de .env. Un archivo en sombra solo contiene marcadores con la forma __ZN_NAME__, así que leerlo por cualquier ruta no revela ningún valor. Otros archivos de credenciales, como ~/.aws/credentials y ~/.ssh/, solo estuvieron expuestos por las formas de cadena larga y de tiempo de espera.
¿Hay una solución provisional para las versiones anteriores a la 1.12.0?
Para las rutas de .env, ejecuta zn env shadow .env en cada carpeta con un archivo .env que tenga valores. Las formas de cadena larga y de tiempo de espera no tienen solución provisional en las versiones anteriores, así que actualizar es la única corrección para ellas.
¿Por qué zn-gate rechaza un comando que no puede comprobar a tiempo en lugar de dejarlo ejecutarse?
Una protección que permite lo que no pudo comprobar le da al atacante una forma de apagarla: hacer el comando lento o largo. Rechazar cuesta un reintento del agente. Permitir cuesta la protección.
¿Dónde puedo leer el aviso?
GHSA-rccq-h3xc-j9qj en GitHub. La prueba de concepto completa no se publica mientras los usuarios actualizan.
Las correcciones están en zn-gate 1.12.0 (MIT, local, sin cuenta), y el resto de la versión está en novedades de zn-gate 1.12.0. Para lo básico sobre cómo mantener un .env fuera del alcance de un agente, consulta cómo impedir con un solo comando que Claude Code, Codex CLI, Cursor y OpenCode lean tu .env. El código está en GitHub, y Instalación cubre cada agente.