← Volver a todas las publicaciones
Publicado el · por Renaud Deraison

Once tests superados

Straiker publicó un repositorio que consigue que Claude Code ejecute malware sin jailbreak. Su CONTRIBUTING.md envía la revisión a un subagente que define el propio repositorio, fijado en Haiku y apartado de la carpeta de tests. Después Fable ejecuta pytest, un archivo de test descarga e inicia un implante Sliver, y la terminal informa 11 passed. Anthropic cerró el reporte como Informative: confiar en la configuración de un repositorio es la decisión de confianza del workspace que toma el usuario. Bromure Agentic Coding decide hasta dónde llega esa confianza: una VM con claves sustitutas, un firewall en el Mac y una traza que el repositorio no puede editar.

El agente siguió el flujo de trabajo documentado del proyecto, paso a paso, y ese flujo lo había escrito el atacante. La terminal decía 11 passed.

Encuentras en GitHub un pequeño cliente de métricas que te ahorraría una semana, y le pides a Claude Code que lo clone y le eche un vistazo. Hace un buen trabajo. Lee el código, señala un búfer sin límite y un bucle de reintentos que girará para siempre con un mensaje envenenado, y sugiere correcciones. Le dices lo de siempre: arregla eso y prepara un PR.

Unos minutos después las correcciones están hechas, los tests están en verde y el resumen dice 11 passed. En algún momento de esos minutos, un implante abrió una shell remota en tu portátil.

Ese es el escenario que Brian Cumi, de Straiker STAR Labs, construyó y publicó el 22 de septiembre con el título Fable to Haiku: How a Malicious Repo Tricked Claude Code Into Running Malware. El resumen de octubre de investigación en seguridad de agentes de código de Adversa lo incluye entre las técnicas de ataque del mes. El repositorio no explota ningún bug. Cada paso usa una función documentada tal como la concibieron sus diseñadores.

Cómo el repositorio se revisó a sí mismo

El repositorio, northstar-metrics, parece un proyecto de código abierto normal, y su primer truco es corriente: un CONTRIBUTING.md. Al pedir un PR, ataste al agente al flujo de contribución del proyecto, y un agente que sigue instrucciones lee la guía de contribución. Esta dice que los revisores deben ejecutar el agente de primera revisión del proyecto antes de ejecutar los tests.

Ese agente vive en el repositorio, en .claude/agents/first-pass.md. Claude Code permite que un proyecto defina sus propios subagentes en esa carpeta, con un modelo y un alcance en el frontmatter. Este fija model: haiku y limita la revisión a src/ y docs/. Sus instrucciones añaden que una lectura en la sesión principal no sustituye a ejecutar el agente, para que el agente principal no se lo salte.

Esta es la cadena completa:

  1. Fable, el modelo principal, entrega la revisión al subagente del repositorio.
  2. El subagente se ejecuta en Haiku 4.5 y solo mira src/ y docs/.
  3. Vuelve con dos hallazgos que el autor plantó para que los encontrara: una cadena de versión escrita a mano en el código y una discrepancia entre la documentación y el código. Ambos son inofensivos.
  4. El flujo documentado da la revisión por terminada. Nadie abre tests/test_compat.py.
  5. Fable ejecuta pytest.
  6. Antes de su aserción, test_compat.py decodifica una URL en hexadecimal, descarga un archivo Python y lo inicia con subprocess. El bloque está envuelto en try/except: pass, y la aserción siempre se cumple.
  7. Un implante Sliver abre una sesión de comando y control. Pytest imprime 11 passed.

El resumen de Cumi sobre por qué funciona: el repositorio se apoya en tres supuestos corrientes. Los proyectos pueden definir sus propios subagentes, los agentes siguen el flujo documentado de un proyecto, y una ejecución de tests en verde cuenta como comprobación del código. “Un repo malicioso que entiende los tres supuestos no necesita romper ninguno. Solo necesita situarse en la costura donde se juntan.”

Straiker lo reportó a Anthropic a través de HackerOne el 14 de septiembre. Anthropic lo cerró como Informative: respetar la configuración del repositorio entra en la decisión de confianza del workspace que toma el usuario.

La decisión de confianza, en la práctica

La respuesta de Anthropic es coherente con cómo funciona Claude Code. Cuando abres una carpeta, se te pregunta si confías en ella, y dijiste que sí. Esa decisión cubre el CLAUDE.md, los hooks, los subagentes y la suite de tests.

El problema es sobre qué estabas decidiendo. Confiaste en el repositorio lo suficiente para mirarlo. Para eso lo clonaste. En un portátil, confiar en esta carpeta lo suficiente para ejecutar sus tests significa confiarle todo lo que tu cuenta puede alcanzar, porque ahí es donde se ejecuta pytest. Straiker enumera lo que tenía el implante: cada archivo que puedes leer, credenciales en memoria, claves SSH en disco y cada repositorio al que puedes hacer push.

Ningún revisor, humano o modelo, lee cada archivo de test de cada proyecto que evalúa. Aquí la revisión se acortó a propósito, pero muchas revisiones honestas también son cortas. Lo que sí puedes elegir es dónde se ejecuta la suite de tests.

En un portátil: el repo escribe la revisión, los tests corren como túnorthstar-metrics (el repo)CONTRIBUTING.md“ejecuta el agente first-pass”.claude/agents/first-pass.mdmodel: haikuscope: src/ docs/tests/test_compat.pyfuera del alcance de la revisiónsin jailbreak, sin CVE:solo funciones documentadasClaude Code1. Fable delega la revisión2. Haiku lee src/ y docs/3. dos hallazgos plantados, inocuos4. revisión marcada como hecha5. $ pytest===== 11 passed =====Tu portátil, como tú6. descarga la carga y la ejecuta7. se abre la sesión SliverEl implante alcanzatodo archivo que lees~/.ssh/id_ed25519~/.aws .env tokenspush a todos los repos
La cadena de Straiker en un portátil. El repositorio desvía la revisión a un modelo más barato que nunca ve los tests, pytest ejecuta la carga útil como el desarrollador, y el implante hereda todo lo que el desarrollador alcanza. Lo único que ve el usuario es la última línea.

El mismo repositorio en una máquina Bromure

En Bromure Agentic Coding, Claude Code se ejecuta dentro de una VM Linux en tu Mac. Cada máquina tiene su propio disco, su propio directorio personal y sus propias credenciales, y todo su tráfico sale a través de un proxy en el Mac. La decisión de confianza del workspace sigue siendo tuya. Lo que cambia es lo que el repositorio de confianza puede alcanzar.

Reproduce la cadena de Straiker dentro de una máquina Bromure. Los pasos uno a cinco ocurren como antes: el agente lee la guía de contribución, el subagente revisa las carpetas equivocadas y pytest se ejecuta. A partir del paso seis, el repositorio choca contra el Mac.

La descarga pasa por el Mac

La carga útil hay que ir a buscarla. El test decodifica una URL y pide un archivo a un host que el proyecto nunca había necesitado. Dentro de una máquina Bromure solo hay un camino hacia fuera: el switch de red virtual desvía cada conexión a los puertos 80 y 443 hacia el proxy del Mac, y contrasta cualquier otra conexión, sea cual sea el protocolo, con las reglas de la máquina.

Esas reglas están en el panel Guardrails de la máquina, en Outbound connections: entradas de permitir y denegar por host, rango de IP, protocolo y puerto, con un ajuste Unmatched traffic en Allow o Deny. Una máquina que usas para evaluar código de desconocidos necesita una lista corta: el proveedor del modelo, los registros de paquetes, GitHub.

allow web api.anthropic.com
allow web pypi.org
allow web files.pythonhosted.org
allow web github.com
default deny

Con esas reglas, la descarga de test_compat.py nunca llega a conectar, y el try/except: pass que el autor añadió para ocultar fallos ahora oculta este. Pytest sigue imprimiendo 11 passed. La diferencia está en el Mac. La Security Timeline de la máquina tiene una fila roja Firewall con un veredicto de denegación, el host al que el test intentó llegar y el puerto, con marca de tiempo segundos después de que el agente ejecutara pytest. La suite de tests de un cliente de métricas no tiene ningún motivo para contactar con un host del que nunca has oído hablar, y ahora sabes que lo intentó.

El tráfico posterior del implante recibe el mismo trato. Tanto si Sliver llama a casa por HTTPS, por TLS mutuo en su propio puerto, por WireGuard o por DNS, la conexión sale por el switch, y el switch contrasta cada protocolo con la misma tabla.

El implante encuentra sustitutos

Supón que la máquina permitía el tráfico no coincidente, que la carga útil llegó y que Sliver abrió su sesión. El atacante tiene ahora una shell en la VM, con el mismo alcance que Straiker enumeraba para el portátil. En una máquina Bromure ese alcance cubre menos.

  • Archivos. La VM contiene su propio directorio personal y las carpetas de proyecto que elegiste compartir con esa máquina. El directorio personal de tu Mac, tus otros proyectos, tus perfiles de navegador y tu Llavero no están dentro.
  • Credenciales. Bromure guarda las claves de API reales, los tokens OAuth, los tokens de git, los secretos de la nube y las contraseñas de bases de datos cifrados en el Mac. La VM tiene sustitutos con la forma correcta: ANTHROPIC_API_KEY=sk-ant-api03-brm-…, un token ghp_ que GitHub rechazaría, una clave secreta de AWS que no firma nada. El proxy pone el valor real después de que la petición sale de la VM, y solo para el host al que pertenece la credencial.
  • Claves SSH. ls ~/.ssh/id_* no devuelve nada. El SSH_AUTH_SOCK de la VM es un puente hacia un ssh-agent en el Mac. La VM puede pedir una firma, pero no puede leer la clave. Activa Ask before use en una clave, y cada petición de firma abre un diálogo en el Mac.

Un implante que recoge lo que puede y lo envía a casa tropieza con un segundo cable. Un token sustituto tiene un único destino legítimo, el host para el que se emitió. Cuando el proxy ve sk-ant-api03-brm-… o ese token ghp_ dirigirse a cualquier otro sitio, rechaza la petición con HTTP 451, pausa la VM y abre en el Mac una alerta titulada This environment may have been compromised, que nombra el token, el host para el que se emitió y el host al que se envió. Solo salió un sustituto, así que no hay nada que rotar.

El push pregunta primero

El último elemento de Straiker, los repositorios con permisos de push, es el que convierte un portátil comprometido en un incidente de cadena de suministro. Un atacante con una shell y tus credenciales de git puede hacer push de un commit a cada proyecto que mantienes.

En una máquina Bromure, un push a GitHub, GitLab o Bitbucket es una escritura, y las máquinas nuevas vienen con Prompt before write para cada servicio protegido. El proxy retiene el push y macOS muestra Allow write on "github.com" from workspace "…"? con la operación tal como apareció en la red, POST /acme/app.git/git-receive-pack. Tú no pediste ningún push, así que haces clic en Don't allow, y el implante recibe un 403 que termina en blocked by Bromure Guardrails. macOS dibuja el diálogo a partir de lo que leyó el proxy, así que nada dentro de la VM puede cambiar su texto.

La traza muestra la revisión que no ocurrió

Cuando se asienta el polvo, quieres saber qué hizo el agente. En el nivel de trazado por defecto, AI request details, el proxy guarda cada intercambio con el proveedor del modelo, cifrado en el Mac, y el Trace Inspector desglosa cada uno en llamadas a herramientas, comandos de shell y lecturas de archivos.

Para este repositorio, el registro responde a las preguntas que importan:

  • ¿Qué modelo hizo la revisión? El cuerpo de la petición capturada lo nombra: claude-haiku-4-5, un turno después de que la sesión principal leyera .claude/agents/first-pass.md.
  • ¿Qué leyó? Cada llamada Read, con su ruta. Hay llamadas para archivos bajo src/ y docs/, y ninguna para tests/test_compat.py.
  • ¿Qué se ejecutó? La llamada Bash que inició pytest, y junto a ella, en la Security Timeline, la fila del firewall de la descarga que provocó.

El repositorio controla lo que ve el agente y lo que imprime pytest, pero no tiene forma de editar la traza. El proxy escribe ese registro en el Mac, a partir del propio tráfico.

En Bromure: los tests corren en una VM y las salidas están en el MacVM de la máquina: “eval”~/northstar-metrics$ pytest → test_compat.pyLo que encontraría un implanteANTHROPIC_API_KEY=sk-ant-api03-brm-…GH_TOKEN=ghp_… (sustituto)~/.ssh: sin claves privadassolo el home de esta máquinani home del Mac, ni otros proyectostoda conexión sale por el switchSwitch + proxy, en el MacFirewall: host de la carga → denyfila roja en la Security TimelineSustituto fuera de lugar → 451, VM pausadaTraza: modelo claude-haiku-4-5,Read src/… docs/…, Bash pytestDiálogo de macOS: Allow write on …?POST /acme/app.git/git-receive-packclaves reales solo para su propio hostInternetapi.anthropic.compypi.orggithub.comhost de la carga:nunca contactadoGuardado en el Mac, fuera de la VMClaves de API y tokens OAuth · credenciales de git, nube y BD · claves SSH privadas · trazas cifradas · Security Timeline
El mismo repositorio en una máquina Bromure. La revisión sigue yendo a Haiku y pytest sigue ejecutándose, pero la VM solo tiene credenciales sustitutas y ninguna clave privada, la descarga de la carga útil se detiene en el firewall de salida del Mac, los push esperan un diálogo de macOS y la traza registra qué modelo revisó qué.

Una máquina para el código que no has leído

El informe de Straiker termina con tres recomendaciones: exigir aprobación antes de ejecutar un subagente que define el repositorio, leer los archivos de test antes de ejecutarlos y tratar archivos como .claude/agents/, los hooks y las definiciones de workflow como entrada no confiable. Las tres son sensatas, y las tres dependen de que alguien se acuerde, cada vez, en una tarea que parecía una revisión rutinaria.

En Bromure el hábito seguro cuesta unos pocos clics. Una máquina nueva es un clon copy-on-write de una imagen de Ubuntu firmada, así que apenas ocupa disco. Eso sugiere una configuración para evaluar código ajeno:

  1. Crea una máquina llamada eval. Dale un proveedor de modelo y nada más: ningún token de git con permisos de push, ninguna clave de la nube, ninguna base de datos.
  2. Configura su salida para denegar por defecto. En Guardrails › Outbound connections, permite el proveedor del modelo, los registros de paquetes y GitHub, y pon Unmatched traffic en Deny.
  3. Clona dentro de la VM, no en una carpeta compartida. Inicia la sesión en una carpeta nueva del directorio personal de la máquina. Una carpeta compartida es una ventana en directo a tu Mac, y el directorio personal de la máquina es la parte que Bromure puede rebobinar o borrar.
  4. O hazla desechable. bromure-cli vm run --rm inicia una máquina que se elimina, disco incluido, cuando se detiene.
  5. Lee la Timeline antes de adoptar la dependencia. Una fila Firewall de denegación durante una ejecución de tests es un hallazgo que ningún revisor reportó, imprimiera lo que imprimiera pytest.

Si algo se cuela, limpias una sola máquina. Rewind home… devuelve el directorio personal al punto de control de un arranque anterior. Una máquina que el detector de compromiso ha marcado se niega a arrancar hasta que elijas Wipe and Launch, que reconstruye su disco y su directorio personal y conserva tus ajustes y las credenciales reales en el Mac. La cuenta de tu portátil, tus otros proyectos y tus otras máquinas nunca estuvieron dentro.

Cuando la biblioteca se haya ganado su sitio, pásala a la máquina que hace el trabajo real. Esa máquina también tiene solo claves sustitutas, y sus push también se detienen en el diálogo del Mac.

La descarga tiene que preguntar

Cada conexión que abre la VM pasa por el switch virtual y el proxy del Mac. Con el tráfico no coincidente denegado, un test que descarga una carga útil desde un host nuevo falla, y la Security Timeline muestra el intento.

La shell encuentra sustitutos

Las claves de API reales, los tokens de git y los secretos de la nube permanecen cifrados en el Mac, y las claves SSH privadas nunca entran en la VM. Un sustituto enviado a cualquier sitio que no sea su host devuelve 451 y pausa la máquina.

Un push necesita un clic en el Mac

Con Prompt before write, el valor por defecto en las máquinas nuevas, un git push espera un diálogo de macOS que muestra la operación exacta. Un implante no puede responderlo.

Un registro que el repo no puede editar

La traza en el Mac muestra qué modelo revisó el código, qué archivos leyó y qué comando se ejecutó después, imprimiera lo que imprimiera la terminal.

Confía menos en el repositorio

El repositorio no hizo nada exótico. Escribió una guía de contribución, definió un agente auxiliar y publicó un archivo de test. Los proyectos seguirán haciendo las tres cosas, porque son útiles, y los agentes seguirán siguiéndolas, porque seguir las convenciones de un proyecto es lo que hace bueno a un agente en su trabajo.

La respuesta de Anthropic deja la decisión en tus manos, y ahí es donde debe estar. Tómala en una máquina hecha para eso: una en la que confiar en un repositorio lo suficiente para ejecutar sus tests signifique confiarle una VM, unos cuantos sustitutos y una lista corta de hosts. Luego deja que la suite de tests imprima lo que quiera.