El token estaba en el mismo heap
Oren Yomtov, de Accomplish, se escapó dos veces del sandbox de Codex, de OpenAI. Una de las fugas partió de read-only, el modo más estricto: JavaScript no confiable llamó a v8.getHeapSnapshot() y leyó el token de autorización del contexto de confianza en el heap que ambos compartían. La otra hizo que apply_patch ampliara su propio permiso de escritura hasta la raíz del sistema de archivos y luego añadió una línea a .zshrc. OpenAI corrigió ambas en ocho días. La propiedad que las produjo no tiene parche: un guardián y el código que guarda eran dos variables en un mismo proceso. Bromure Agentic Coding ejecuta el guardián en tu Mac y el agente en una VM.
El contexto no confiable nunca rompió el sandbox. Le pidió a Node una instantánea del heap, encontró allí el token de autorización del propio sandbox como una cadena corriente, y lo usó.
Apuntas Codex a un repositorio que alguien te envió. Eres cuidadoso, así que lo
ejecutas en read-only, el modo en el que el agente puede mirar y no puede
tocar. El agente lee el código. Parte de ese código es una carga maliciosa, y
unos segundos después hay un comando ejecutándose en tu Mac, fuera del sandbox,
sin ningún aviso en pantalla.
Eso es Heapjack, una de las dos fugas del sandbox de Codex que Oren Yomtov, de Accomplish, publicó este mes. BleepingComputer las cubrió el 20 de septiembre. Yomtov reportó ambas a OpenAI el 12 de agosto, y OpenAI las corrigió en ocho días: Heapjack en Codex Desktop build 26.818.21641, la otra en Codex CLI 0.149.0. La declaración de OpenAI es de las cortas: “Agradecemos a los investigadores habernos contactado y compartido sus hallazgos. Abordamos ambos problemas en agosto, pero estamos reforzando continuamente nuestros sandboxes.”
Por qué existía el primer fallo es la parte que sobrevive al parche.
Un proceso, un heap, un token
Codex Desktop tiene una herramienta llamada node_repl que ejecuta JavaScript.
Mantiene dos contextos dentro: uno de confianza, al que se le permite pedirle al
proceso padre que haga cosas reales, y uno no confiable, que ejecuta lo que sea
que el agente esté evaluando. La separación entre ambos es el sandbox.
Los dos contextos viven en el mismo proceso Node. En palabras de Yomtov: “Un proceso Node, un isolate V8, un heap compartido por ambos contextos. El token es una cadena en ese heap.”
El token es la forma en que el proceso padre los distingue. Una petición que llega por la tubería con el token correcto es de confianza y se ejecuta; una sin él, no. Es un UUID aleatorio, que es un secreto perfectamente bueno mientras el código del que lo estás ocultando no pueda leer tu memoria.
Puede. v8.getHeapSnapshot() es una API pública de Node, una herramienta de
perfilado, y devuelve un volcado de cada objeto del heap, cadenas incluidas. El
contexto no confiable la llama, filtra lo que tiene forma de UUID y acaba con un
puñado de candidatos. Aislar el verdadero requiere un paso más: probar cada uno
contra el padre y observar qué vuelve. Un token equivocado responde “no
autorizado”. El correcto responde con una queja sobre el resto de la petición.
Yomtov se queda con la cadena que produce la segunda respuesta.
Con el token en la mano, el contexto no confiable escribe peticiones en la misma
tubería stdout que usa el de confianza. El padre comprueba el token, lo
encuentra válido, y ejecuta el comando fuera del sandbox. A partir de ahí:
comandos de sistema mediante open, el socket de Docker, ediciones a archivos
de configuración.
Todo eso desde read-only. Citando a Yomtov de nuevo: “Todo ello se ejecutó en
read-only, el modo de sandbox más estricto, donde se supone que el agente no
puede escribir nada.”
Un permiso de escritura que crece
Overpatch, la segunda fuga, es más simple. Codex CLI tiene una herramienta
apply_patch para editar archivos, y deduce cuánto acceso de escritura necesita
a partir de las rutas nombradas en el parche que le entregaron. Las rutas vienen
del modelo, y el modelo está leyendo el repositorio.
Así que un parche nombra /tmp en una entrada, con un cambio que no hace nada.
El cambio falla. La concesión de permiso que implicaba, no: el acceso de
escritura se amplía hasta la raíz del sistema de archivos. Una segunda entrada
del mismo parche escribe entonces a través de un enlace simbólico dentro de
$HOME y añade una línea a .zshrc.
Nada se ha escapado todavía. La fuga ocurre después, cuando abres un terminal y
tu shell ejecuta esa línea como tú, sin sandbox alguno en el camino. Esta se
ejecutó en workspace-write, el modo de todos los días.
Dos fallos distintos, dos productos distintos, una frase del artículo que cubre ambos: “Lo que hacía cumplir las reglas estaba sentado dentro de aquello sobre lo que las hacía cumplir.”
El guardián y lo guardado, en un mismo espacio de direcciones
Un sandbox que vive en el mismo proceso que el código al que refrena es una convención. Se sostiene hasta que alguien la mira de cerca. El token está en el heap. El cálculo de permisos lee rutas suministradas por el atacante. La tubería es la misma tubería. Cada uno de esos puntos es reparable, y OpenAI reparó cada uno en ocho días. El próximo fallo de este tipo será otra cadena en el mismo heap.
La conclusión del propio Yomtov es la arquitectónica: “Por eso Accomplish ejecuta el agente entero dentro de una VM… Tratamos todo lo que hay en el invitado como no confiable, root incluido, porque nada de lo que decide qué está permitido se está ejecutando ahí dentro con él.”
Ese es el diseño que entrega Bromure Agentic Coding. El agente se ejecuta en un invitado Ubuntu 24.04 bajo Virtualization.framework de Apple, el hipervisor que el propio macOS proporciona. El invitado es no confiable por construcción, root incluido. El código que decide qué está permitido se ejecuta como un proceso del host al otro lado de un vsock, y ningún token dentro del invitado autoriza nada, porque todo lo que vale la pena autorizar está fuera del invitado.
Con qué se encuentra cada fuga en su lugar
Overpatch es el caso concreto, así que empieza por ahí. El truco termina
añadiendo texto a .zshrc en $HOME. En un espacio de trabajo Bromure, el
$HOME del invitado es home.img, una imagen ext4 adjunta a esa VM. El
directorio personal de tu Mac no está dentro. Lo que el invitado puede alcanzar
en tu Mac son las carpetas de proyecto que compartiste con él: hasta ocho,
elegidas por ti, listadas en el editor del espacio de trabajo. Los dotfiles que
tu shell lee cuando abres el Terminal no están entre ellas, a menos que los
pongas ahí.
Heapjack es el caso más difícil. Nada en Bromure detecta un truco de instantánea del heap dentro del invitado, y nada pretende hacerlo. Lo que cambia es dónde aterriza la fuga. En Codex, el código escapado aterrizaba en la cuenta que guarda tus claves SSH y tus credenciales de nube. En un espacio de trabajo Bromure, aterriza en una VM Linux donde:
Las credenciales son señuelos
sk-ant-api03-brm-…, una cadena con forma de ghp_, un perfil ~/.aws
cuyo secreto es inválido. Preservan la estructura, así que las herramientas
las aceptan, y no valen nada aguas arriba. Los valores reales viven en la
memoria del proxy en el host y se sustituyen a la salida, acotados al host
exacto o a su subdominio.
Sacar una dispara la alarma
El proxy examina cabeceras y cuerpos de cada petición saliente en busca de los señuelos propios del espacio de trabajo. Un señuelo que va hacia donde no fue acuñado se bloquea con un HTTP 451, sin reenviar ni un byte. La VM se pausa, y una fila roja Intermediación de credenciales aparece en la Cronología de seguridad.
La red es una lista que escribiste tú
La salida es un conjunto ordenado de reglas que termina en default allow o
default deny. El conmutador virtual desvía los flujos de la VM en los
puertos 80 y 443 hacia el proxy del host, sin ninguna variable de entorno de
por medio y sin nada que el invitado pueda desactivar. Los cambios se
aplican de inmediato a una sesión en curso.
Un push es una pregunta, hecha en el Mac
Las credenciales de forja Git llevan una política de escritura, y los
espacios de trabajo nuevos vienen por defecto en Preguntar antes de
escribir. git push es una escritura. El diálogo nombra la operación
exacta y se dibuja en tu escritorio. Un atacante dentro de la VM no puede
hacer clic en él.
Cuando sale mal, te recuperas con un elemento de menú. El flujo de compromiso borra el disco y el home del espacio de trabajo y conserva tus ajustes, tokens y claves SSH. Las credenciales reales nunca salieron de tu Mac, así que no rotas nada. Restablecer disco vuelve a clonar la imagen del sistema. Ninguna acción de Bromure toca tus carpetas de proyecto compartidas, y el aviso lo dice.
El modo más estricto fue el que perdió
El desarrollador de esta historia lo hizo todo bien. Eligió el ajuste más
estricto que ofrece la herramienta, a propósito, porque el repositorio no era
suyo. read-only es una función real e hizo un trabajo real, hasta que una
cadena en un heap compartido la convirtió en una sugerencia.
Los modos, las listas de permitidos y los avisos de aprobación merecen la pena, y cada uno de ellos asume que el código que los hace cumplir queda fuera del alcance del código al que restringe. Eso es una afirmación sobre la disposición de la memoria, y puedes ir a comprobarla. Yomtov comprobó Codex dos veces, en dos productos, y la respuesta volvió que no ambas veces.
Así que ejecuta el agente en algún sitio donde la respuesta deje de importar. Dale root, déjale instalar lo que quiera, déjale leer el repositorio que alguien te envió, y mantén cada credencial, cada decisión de destino y cada confirmación de push al otro lado de un hipervisor, donde la próxima fuga aterriza en una máquina que ya estabas dispuesto a tirar.