Volver a todas las publicaciones
Publicado el · por Renaud Deraison

El sandbox abrió el puerto

CVE-2026-65105 es un fallo de red en NVIDIA NemoClaw, una herramienta cuyo propósito entero es ejecutar un agente de IA dentro de un sandbox. El sandbox es un contenedor Docker, los contenedores no pueden alcanzar la interfaz de loopback del anfitrión, así que NemoClaw arrancó el servidor de modelo local en todas las interfaces — lo que apagó justo la única comprobación que habría impedido que una página web hablara con él. Lo que una página de paso se llevó fue la plantilla de conversación del modelo, la capa que compone cada prompt de sistema antes de que el modelo lo lea. En Bromure Agentic Coding el motor local escucha en loopback, en un puerto que el invitado nunca conoce, y el invitado lo alcanza por vsock en lugar de por la red.

Fue la contención la que abrió el agujero. Un sandbox que no puede alcanzar la interfaz de loopback de tu máquina empuja el servidor de modelo a todas las interfaces, y a partir de ahí una pestaña del navegador también lo alcanza.

Estás ejecutando tu agente de codificación de la manera cuidadosa. Vive en un sandbox. El modelo con el que piensa corre en tu propia máquina, así que ningún prompt sale del edificio. Has hecho las dos cosas que todo el mundo te dice que hagas.

Entonces abres una pestaña del navegador, y desde ese momento tu agente empieza a escribir código con un defecto dentro que se abstiene de mencionar.

Eso es CVE-2026-65105, divulgada el 25 de agosto por Elad Luz y Ofek Itach, de Oasis Security, cuyo análisis lo llama secuestro de agente al paso. Lo llevaron al PSIRT de NVIDIA antes de publicar, y la historia salió el mismo día en The Hacker News, SiliconANGLE y CSO Online. NVIDIA tiene un aviso y un parche.

El producto es NemoClaw, la herramienta de NVIDIA para desplegar un agente OpenClaw dentro de un sandbox OpenShell con un backend Ollama local para la inferencia. La contención es el argumento de venta.

El contenedor no podía alcanzar el loopback

Ollama escucha en el puerto 11434 y, por defecto, se enlaza a 127.0.0.1 — la interfaz de loopback, alcanzable solo desde la propia máquina.

Los sandboxes OpenShell son contenedores Docker. Un contenedor tiene su propio espacio de nombres de red, así que 127.0.0.1 dentro del contenedor es el contenedor, no el anfitrión. El agente del sandbox, por tanto, no puede alcanzar un servidor de modelo enlazado al loopback del anfitrión.

NemoClaw lo resolvió con una variable de entorno. Lanza Ollama con OLLAMA_HOST=0.0.0.0:11434: todas las interfaces de la máquina, incluida la que da al wifi de la oficina. El instalador seguía imprimiendo Using Ollama on localhost:11434 mientras el socket le respondía al mundo.

Luego viene el paso que convierte una decisión de configuración en una vulnerabilidad. La API de Ollama no tiene autenticación, así que lleva dos guardas en su lugar. La primera es una comprobación CORS sobre la cabecera Origin. La segunda es una lista blanca de la cabecera Host que acepta localhost, el nombre de host de la propia máquina y los nombres terminados en .localhost, .local o .internal.

La segunda guarda contiene una excepción. Ollama mira la dirección a la que está enlazado y, si esa dirección no es loopback, se salta la comprobación de Host por completo. Se ve por qué alguien escribió eso: quien enlaza una interfaz pública quiere clientes remotos, y una lista blanca de Host pensada para loopback los rechazaría a todos. El resultado es que el ajuste que expone el puerto es el mismo ajuste que desarma la comprobación que lo protege.

Queda la comprobación CORS, y el DNS rebinding atraviesa CORS por construcción. El atacante sirve su página desde un dominio que controla, en el puerto 11434, y después hace que ese dominio vuelva a resolver a 127.0.0.1. Origin y Host son ahora el mismo nombre elegido por el atacante, que Ollama lee como mismo origen. Cubrimos el mecanismo del rebinding en detalle la semana pasada, así que dalo por sentado: una página que visitas se convierte en cliente de un servicio de tu máquina, y tarda alrededor de un minuto.

El rebinding es la vía de entrada interesante. También es la más difícil. Un servidor en 0.0.0.0 responde a todos los demás dispositivos del segmento de red sin truco de navegador alguno: el portátil del invitado, la tele conectada, la máquina comprometida dos mesas más allá.

la restricciónsandbox OpenShell = contenedor Dockerespacio de nombres de red propio, así que127.0.0.1 = el contenedor, no el anfitriónel agente no alcanza el servidor de modeloun problema real, que pedía una respuesta realla respuesta que entregó NemoClawOLLAMA_HOST=0.0.0.0:11434todas las interfaces, sin autenticaciónel instalador imprime: “Using Ollama on localhost”las dos guardas de Ollama1 · comprobación CORS de la cabecera Origin2 · lista de Host: localhost, nombre de host,.localhost .local .internalla guarda 2 se salta si el enlace no es loopbackdos vías de entradauna página: reenlazar el dominio a 127.0.0.1,Origin y Host coinciden y CORS pasala LAN: marcar el puerto directo, sin trucoambas llegan sin autenticar a la API enteraLa configuración que expuso el puerto es la misma configuración que apagó la comprobación que lo protegía.
Por qué se movió el puerto. El sandbox es un contenedor Docker, así que no puede alcanzar la interfaz de loopback del anfitrión, y NemoClaw lo resolvió enlazando el servidor de modelo a todas las interfaces. Ollama se salta su comprobación de la cabecera Host siempre que la dirección de enlace no es loopback, lo que deja solo una comprobación CORS — y el DNS rebinding hace que Origin y Host sean el mismo nombre elegido por el atacante.

No querían ejecución de código

Alcanzar la API es la mitad aburrida. Lo que hizo Oasis una vez dentro es la parte nueva.

El movimiento obvio es escribir un prompt de sistema malicioso dentro del modelo. Aquí falla: OpenClaw envía su propio prompt de sistema en el array messages en cada petición, y el prompt del cliente gana al que está guardado en el modelo. Así que los investigadores bajaron una capa.

El endpoint /api/create de Ollama acepta un campo template. Una plantilla de conversación es una plantilla Go que convierte el array estructurado messages — roles, contenidos, definiciones de herramientas — en el único bloque de texto plano que el modelo lee de verdad. Se ejecuta en el momento de la inferencia, después de que el cliente haya entregado sus mensajes, y los toca todos.

El ataque son tres peticiones. Leer la plantilla real con /api/show. Conservar su renderizado de herramientas y sus tokens especiales exactamente como están, y cambiar solo cómo se renderizan los mensajes de sistema, añadiendo una instrucción de tu elección. Volver a escribirla con /api/create.

El cliente sigue enviando su propio prompt de sistema cuidado. La plantilla envuelve ese prompt en el del atacante, cada vez, antes de que el modelo vea un solo token de él. En palabras de Elad Luz, citadas por CSO Online, la instrucción queda “una capa por debajo de cualquier cosa que un guardrail o un operador pueda ver”.

La edición aterriza en la definición del modelo en disco, así que sobrevive a la conversación, al reinicio y al reseteo. Los metadatos del modelo, su tamaño y las capacidades listadas siguen iguales, y nada parece distinto en una lista de modelos instalados. Todo consumidor de ese modelo la recibe: la CLI que tecleas a mano y el agente que corre mientras estás comiendo.

Oasis enumera para qué sirve una instrucción así. Nada de eso es lo bastante dramático como para notarse.

Colar el fallo

Producir código con un defecto sutil dentro — de los que se leen bien en revisión, porque se escribieron para leerse bien en revisión.

Callarse

No señalar nunca un problema de seguridad. Un agente que antes te avisaba de algo y ya no lo hace es difícil de detectar.

Dirigir la elección

Recomendar un paquete concreto, una URL concreta, una configuración concreta. Pediste una sugerencia; te dieron una.

Mandarlo a casa

Donde el agente tenga acceso de red, enviar el contenido de las conversaciones y de los archivos a un endpoint elegido por el atacante.

El resto de la API sin autenticar es reparto secundario: /api/generate y /api/chat para ejecutar inferencia en la GPU de otro, /api/pull para llenar un disco, /api/delete para destruir los modelos, /api/push para publicar bajo la cuenta de ollama.com de la víctima, y /api/me para conocer el nombre de host, la clave pública y el usuario conectado de aquel sobre el que acabas de aterrizar.

El sandbox nunca fue el radio de impacto

Oasis es cuidadosa con lo que el sandbox hizo y no hizo. El sandbox OpenShell funciona: aísla el sistema de archivos, la red y los procesos, y mantiene limpio el equipo. Un agente dentro de una organización se gana su sitio sosteniendo control de versiones, CI/CD, APIs internas, cuentas en la nube, chat y una pila de servidores MCP. Toma el agente y diriges ese acceso. El resumen del propio análisis es que el verdadero radio de impacto sigue a los recursos que el agente está autorizado a alcanzar, no a la frontera de su sandbox.

Que es la pregunta que un contenedor nunca te responde, y aquí llega con un giro. La capa de contención es lo que creó la exposición. El espacio de nombres de red de Docker hizo su trabajo. Rodearlo puso el servidor de modelo en el wifi.

Dónde vive el modelo local en un perfil

Bromure Agentic Coding también ejecuta inferencia local. Los agentes de un perfil pueden apuntarse a un modelo en el dispositivo en lugar de a un proveedor en la nube, que es exactamente la función que ofrece NemoClaw. La fontanería de debajo está construida al revés, y cada paso de ella es una respuesta directa a algo de lo anterior.

El motor se enlaza a loopback, y nunca a 0.0.0.0. El servidor de modelo en el dispositivo corre en el Mac, en 127.0.0.1, en un puerto que el kernel reparte al arrancar en vez de un número fijo. El puerto dinámico está ahí por una razón sosa: para que Bromure no pelee nunca con lo que ya ocupe el tradicional 11434 en ese Mac, Ollama y LM Studio incluidos. También significa que no hay un puerto bien conocido al que una página pueda apuntar.

El invitado no lo alcanza por una red. Dentro de la VM, el agente marca 127.0.0.1:11434, la dirección que espera. Esa conexión no se acerca a ninguna ruta. Un puente en la VM empalma el flujo TCP a vsock, el transporte de socket de virtio, en el puerto 8446, y el extremo del canal en el anfitrión se lo entrega al motor en loopback. Un canal vsock es una tubería entre un hipervisor y un invitado. No tiene dirección IP, ni nombre de host, ni registro DNS, así que no hay nada que reenlazar, nada que escanear, y ninguna forma de que un navegador lo marque. El invitado nunca conoce en qué puerto está el motor.

Nada de tu LAN puede alcanzar la VM. En Resources → Network, el modo por defecto es NAT: la salida funciona, y nada de tu red local puede alcanzar la VM. Eso cierra la mitad del hallazgo de NemoClaw que no necesita reenlace alguno, el dispositivo en el mismo wifi marcando el puerto directamente, y lo cierra sin que cambies un ajuste.

No hay ninguna definición de modelo en la VM que reescribir. Los agentes invitados están fijados a un único identificador de modelo, bromure-local, y lo direccionan en bromure.llm, un nombre de host sintético sin registro DNS detrás. El proxy de la VM intercepta ese nombre y lo reenvía al anfitrión. Qué modelo responde es un mapeo que tú mantienes en el anfitrión, en el panel Local Models del perfil, y cambiarlo es un remapeo del lado del anfitrión: ninguna reconfiguración dentro de la VM, ningún reinicio de agente. La plantilla, los pesos y el enrutado viven todos en el Mac. Nada en el invitado tiene un equivalente a /api/create, porque nada en el invitado tiene el modelo.

La inferencia local pasa por la misma tubería que la de nube. Como ese tráfico va por el proxy de Bromure en vez de directo a un puerto, recibe el mismo trato que una llamada a Anthropic o a OpenAI: la misma traza, el mismo registro y la misma detección de inyección de prompt. El panel Prompt Injection puntúa el contenido de archivos, las páginas web y las salidas de herramientas que el agente lee, con un modelo PromptGuard local, y puntúa los archivos CLAUDE.md, AGENTS.md y GROK.md que carga como autoridad, con un clasificador ModernBERT afinado más un escáner determinista de cargas en Unicode invisible. Las detecciones van al Security Log, o pausan la petición, o devuelven un 451 seco, según cuál de las tres respuestas hayas elegido.

Un puerto en todas las interfaces¿dónde escucha el motor?0.0.0.0:11434 · fijo, bien conocido¿cómo lo alcanza el sandbox?por IP, como todos los demás en el wifi¿quién más puede alcanzarlo?una página que reenlaza, y la LAN, sin autenticar¿quién puede reescribir la plantilla?cualquier cosa que alcance el puerto, víaPOST /api/createUn canal sin dirección¿dónde escucha el motor?127.0.0.1:<puerto del kernel> · solo anfitrión¿cómo lo alcanza la VM?vsock 8446 — una tubería, no una red¿quién más puede alcanzarlo?nadie: sin IP, sin nombre de host, sin DNS¿quién elige el modelo?tú, en el panel Local Models del perfil;el invitado solo conoce el nombre bromure-local
Dos maneras de darle un modelo local a un agente. A la izquierda, un puerto HTTP en todas las interfaces, sin autenticación, con una definición de modelo escribible detrás — alcanzable por cualquier dispositivo de la red y por cualquier página que reenlace un nombre de host. A la derecha, un motor en loopback en un puerto asignado por el kernel, alcanzado desde el invitado por un canal vsock que no tiene dirección que marcar, con la elección del modelo guardada en el anfitrión.

Ahora supón que el modelo te está mintiendo

Lleva un argumento de arquitectura lo bastante lejos y alguien debería ponerlo a prueba. Así que dale la victoria al atacante por la vía que prefieras: una plantilla envenenada, un mal día en un proveedor de modelos, una inyección de prompt que se coló. Tu agente trabaja ahora para otro y su salida se sigue leyendo bien. Pasa esas cuatro cargas por un perfil.

Dirigir la elección, y que la elección siga siendo juzgada. Esta carga es la que más se apoya en la autoridad del modelo, y la que más pared encuentra. Bromure evalúa cada descarga de paquete en el proxy del anfitrión antes de que el agente vea la respuesta, en npm, PyPI, Cargo, RubyGems, Maven, NuGet, Go modules y Packagist, y el proxy no pregunta quién sugirió el paquete. Una barrera de edad de dos días viene activada por defecto, lo que cubre casi todo lo que necesita un typosquat recién publicado. Activa la consulta OSV, apunta el filtrado de paquetes a socket.dev o Delpi, y quita los scripts de instalación, y una recomendación tendrá que sobrevivir a cuatro opiniones en las que el modelo no tiene voz. Los archivos .npmrc y pip.conf dentro de la VM pueden apretar esos ajustes. No pueden aflojarlos.

Mandarlo a casa, por un cable que tú controlas. La exfiltración necesita salida. Guardrails → Outbound connections es una tabla de reglas al estilo pf — una acción, un protocolo (tcp, udp, web, any), un host o CIDR, una lista de puertos, y para web una lista de métodos HTTP — evaluada de arriba abajo, gana la primera coincidencia, con Unmatched traffic en Allow o Deny. Ponlo en Deny, lista los hosts que tu trabajo necesita, y el endpoint del atacante no está entre ellos. La aplicación ocurre en el conmutador virtual y en la capa SNI del anfitrión, así que nada de lo que el invitado le haga a su propio enrutado cambia el veredicto.

Colar el fallo, y luego intentar publicarlo. Guardrails lleva además un modo para cada servicio que el agente puede alcanzar: Block destructive o Read-only en GitHub, GitLab, Bitbucket, AWS, Kubernetes, DigitalOcean, registros de contenedores y cada endpoint de base de datos configurado. En modo solo lectura un git push se clasifica como escritura y se bloquea; un fetch siempre se permite. El commit con puerta trasera se queda en la VM.

Y el acceso que hereda es falso. El punto de Oasis sobre el radio de impacto es aquel en el que un perfil gasta más esfuerzo. Las credenciales de la VM son señuelos: marcadores brm_… como variables de entorno, un ~/.kube/config sintético con certificados de cliente desechables, un blob base64 falso en ~/.docker/config.json, y peticiones AWS refirmadas en el anfitrión, de modo que todo lo que esquive el proxy obtiene InvalidSignatureException en lugar de acceso. Las claves SSH privadas no están nunca en la VM — el anfitrión firma a través de un agente por perfil. Activa Require approval to use para una credencial dada y cada sustitución se convierte en un diálogo del anfitrión con una concesión acotada en el tiempo: cinco minutos, una hora, el resto de la sesión. El uso compartido de carpetas está limitado a ocho directorios que eliges a mano, así que el resto de tu Mac no está ahí para leerlo.

Y puedes verlo todo. La ventana Security Log (Window → Security Log…) es una única tabla cronológica del lado del anfitrión: veredictos de paquetes, cada destino que la VM abrió y si se permitió o se denegó, sustituciones de credenciales, detecciones de inyección. El código invitado no puede editarla, porque el código invitado no puede alcanzarla. Cuando acaba el día, Erase home… y Reset to base… en Resources → Storage devuelven el perfil a su estado justo tras la clonación.

Actívalo

Si ejecutas NemoClaw, aplica el parche de NVIDIA — devuelve Ollama al loopback y pone un proxy con token delante de todo lo que legítimamente necesite salir. The Hacker News señala que la ruta de Windows y WSL no queda del todo cubierta por esa versión, así que comprueba tu plataforma contra el aviso en vez de suponerlo. Luego ve y encuentra todos los demás servidores de modelo de tu máquina y pregúntales a qué dirección están enlazados. La respuesta es 0.0.0.0 más a menudo de lo que la gente espera, porque los contenedores no dejan de pedirlo.

En un perfil, los ajustes que valen dos minutos son la lista corta de siempre. Guardrails → Outbound connections con Unmatched traffic en Deny. Credentials → Require approval to use en cualquier cosa que pueda gastar dinero o borrar datos. Supply Chain → OSV vulnerability check y filtrado socket.dev o Delpi si tienes clave, encima de la barrera de edad que ya está activa. Prompt Injection → los dos detectores, para que el tráfico hacia tu modelo local se lea con el mismo cuidado que el de un modelo en la nube.

Sigue ejecutando el modelo en tu propia máquina. Esa elección te compra prompts que se quedan en local, y vale la pena. No dice nada sobre quién más puede hablar con el servidor que los guarda, que es una pregunta sobre un socket, y un socket merece mejor respuesta que una variable de entorno puesta para tener contento a un contenedor. Instala Bromure Agentic Coding, elige tu modelo en el anfitrión, y déjale al invitado una tubería en lugar de un puerto.