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

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.”

La frontera y lo que limita, en un mismo espacioun proceso Node, un heap V8contexto de confianzapuede pedir al padre que actúeguarda el tokencontexto no confiableejecuta el código del repoesto es el sandboxel heap que ambos comparten"a1f3…-…-…" el token de autorización, una cadenav8.getHeapSnapshot()lo que hace el lado no confiable1. capturar el heap, guardar los UUID2. "no autorizado" vs error de validación3. forjar peticiones en el pipe stdoutel proceso padretoken válido → ejecutar, sin sandboxcomandos en el Macdesde read-only, sin aviso
Heapjack no rompe el sandbox; lee la llave del sandbox. El contexto de confianza, el contexto no confiable y el token de autorización que los distingue viven todos en un único heap V8, dentro de un único proceso Node. v8.getHeapSnapshot() es una API pública de perfilado que devuelve todas las cadenas de ese heap. El contexto no confiable recoge los candidatos con forma de UUID e identifica el token verdadero por la diferencia entre una respuesta 'no autorizado' y un error de validación, y luego forja peticiones de confianza en la tubería stdout compartida. El proceso padre valida el token y ejecuta el comando fuera del sandbox, en modo read-only, donde se supone que nada es escribible.

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.

un sandbox dentro del procesoun solo sistema, una sola cuentael agenteleyendo su repositoriola políticaun token en el mismo heap~/.ssh ~/.aws ~/.zshrc el llavero cada repouna cadena leída de memoriay la separación se acabó,todo lo de arriba está al alcanceuna frontera en el hipervisorel invitado Linux, no confiable, root incluidoel agente, el repo, su propio $HOMEghp_… sk-ant-… señuelosVirtualization.framework + vsockel Mac, donde se decideel proxyclaves reales, reglas,el 451 a la salidael diálogo"¿Permitir escritura …?"renderizado fuera de la VM
Lo que importa es qué máquina evalúa las reglas. A la izquierda: el guardián, el código guardado, el token de autorización y tus claves SSH, credenciales de nube y el .zshrc están todos en un mismo sistema operativo, de modo que leer una sola cadena termina con la separación. A la derecha: el agente recibe una VM Linux cuyas credenciales son señuelos que preservan la estructura, cuyos flujos en los puertos 80 y 443 el conmutador virtual desvía a un proxy del host que el invitado no puede desactivar, y cuyo peor desenlace es un disco que borras. El compromiso total del invitado, root incluido, le da al atacante el 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.