Volver a todas las publicaciones
Publicado el · por Renaud Deraison

La página web que se llamaba localhost

El 17 de agosto, CISA añadió la CVE-2025-62593 de Ray a su catálogo de Vulnerabilidades Explotadas Conocidas. Es ejecución remota de código en la propia máquina del desarrollador, entregada por una página web que visitó, alcanzando un servicio que solo escuchaba en 127.0.0.1. Los agentes de código llevan todo el año llenando esa interfaz de servidores sin autenticación. Bromure Agentic Coding los pone donde ninguna pestaña del navegador puede marcar.

Tu servidor de desarrollo, tu notebook y tu panel local enlazan cada uno un puerto sin autenticación a 127.0.0.1, con el razonamiento de que solo quien está al teclado puede alcanzarlo. Una página web también puede. La política del navegador que se interpone se deshace en unos sesenta segundos.

El 17 de agosto, CISA añadió una vulnerabilidad a su catálogo de Vulnerabilidades Explotadas Conocidas y dio a las agencias federales hasta el 20 de agosto para resolverla. The Hacker News lo contó a la mañana siguiente. La entrada es la CVE-2025-62593, un fallo de inyección de código en Ray, el framework de cómputo distribuido que buena parte de la industria usa para entrenar y servir modelos.

El código se ejecuta en el portátil del propio desarrollador. Alguien tenía Ray corriendo en local para su trabajo, abrió una página web, y la página ejecutó un comando por él.

Tres hechos corrientes, mal colocados

El aviso lo puntúa con 9,4 y describe una cadena sin ningún paso exótico dentro.

La API de Jobs de Ray acepta un comando de shell. Eso es el producto: haces POST a /api/jobs/ con un entrypoint y el clúster ejecuta el comando. El panel escucha en el puerto 8265, y ni él ni los endpoints de jobs exigen autenticación. Fue una decisión deliberada de los mantenedores, cuya postura es que la seguridad y el aislamiento deben imponerse fuera del clúster de Ray. Lo pones en algún sitio donde solo lleguen llamantes de confianza, y la interfaz de loopback de un portátil suena a un sitio así.

Ray sí llevaba una guarda contra los navegadores. Comprobaba si la cabecera User-Agent empezaba por Mozilla y rechazaba esas peticiones, dando por supuesto que un navegador no puede mentir sobre su propio User-Agent. En Firefox y Safari, la API Fetch permite a una página fijar esa cabecera con el valor que le apetezca.

Queda la política del mismo origen, el único control que se interpone entre una página de internet y un servicio de tu máquina. El DNS rebinding la desmonta. El atacante sirve la página desde un dominio cuyos registros DNS controla, con un tiempo de vida muy corto. La página carga y luego vuelve a resolver ese mismo dominio a 127.0.0.1. El navegador la sigue tratando como el mismo origen, mismo hostname y mismo puerto, mientras cada petición va ya a tu máquina. La prueba de concepto publicada usa singularity, un framework de rebinding que lleva años siendo público.

Encadena todo eso y una página maliciosa, o un anuncio envenenado en una página en la que confiabas, publica un job en tu instancia de Ray con un comando de shell dentro. Chrome está protegido por un bug sin relación; Firefox y Safari no lo están. Avi Lumelsky, de Oligo, teorizó el bypass del User-Agent; Jonathan Leitschuh, entonces en Socket, construyó la cadena de rebinding y la divulgación. Ray 2.52.0 lo corrige y añade autenticación por token opcional, desactivada por defecto.

Cómo una página se vuelve cliente de tu portátil1 · cargas la páginaevil.example → 203.0.113.7TTL: 1 segundo2 · el registro cambiaevil.example → 127.0.0.1mismo origen, otro destino3 · guarda = una cabecerafetch(url, headers: UA)Firefox y Safari lo permiten4 · POST /api/jobs/127.0.0.1:8265entrypoint = shellEN QUÉ CONFIABA CADA CAPARay“la red que nos rodea es de fiar”la comprobación de User-Agent“un navegador no miente sobre esto”política del mismo origen“un hostname es una dirección”Solo la tercera es un control de seguridad, y el rebinding es más viejo que casi todas las herramientas que se apoyan en él.
La cadena de rebinding. Cada paso es un comportamiento documentado de una herramienta bien mantenida: un TTL de DNS corto, una cabecera Fetch que la especificación permite fijar a una página, y un endpoint de API cuyo propósito entero es ejecutar comandos.

Los atacantes llegaron antes que el aviso. La botnet RondoDox recogió el fallo dos días antes de la divulgación pública, y la investigación ShadowRay 2.0 de Oligo sigue una campaña que convierte los despliegues de Ray en una botnet autopropagante: los clústeres comprometidos escanean en busca de más instancias de Ray y las infectan, un cron job descarga cargas nuevas desde repositorios controlados por el atacante cada quince minutos, y la carga mina Monero, abre shells inversas y se lleva lo que encuentra. Oligo contó más de 230.000 servidores Ray expuestos, diez veces más que en su primer informe de 2024. Un solo clúster comprometido entregó 240 GB de código fuente, modelos y conjuntos de datos.

Qué está escuchando en tu máquina

Deja Ray a un lado y cuenta otra cosa: los servidores HTTP sin autenticación enlazados a tu interfaz de loopback ahora mismo, y cuántos de ellos sabrías nombrar sin mirar.

Ejecuta lsof -iTCP -sTCP:LISTEN -P después de una semana de trabajo y la lista sale más larga de lo que supondrías. Vite en el 5173. Un servicio FastAPI bajo uvicorn --reload en el 8000. Jupyter en el 8888, mlflow ui en el 5000, el puerto publicado de un contenedor de Postgres, un servidor MCP en el 3000, un puerto de depuración que dejó abierto una tanda de tests que se cayó. Ninguno pregunta quién eres. Sus autores hicieron la suposición que hicieron los mantenedores de Ray, y aguanta hasta que una pestaña de otra ventana se pone a hacer peticiones.

Los agentes cambiaron el volumen. Un agente que trabaja un ticket arranca servicios como cosa natural. Levanta el servidor de desarrollo para comprobar su propio cambio, sube una base de datos para pasar la migración, arranca el notebook para mirar los datos, lanza la API para golpear un endpoint. Lo hace varias veces por tarde, en varios espacios de trabajo, y no recoge nada, porque nada se lo dice. Los procesos sobreviven a la tarea. Para el jueves la interfaz lleva una docena de servicios que no arrancaste tú y que no podrías enumerar de memoria, cada uno enlazando sin autenticación porque la guía rápida del framework dice que en localhost no pasa nada.

La postura de Ray se lee distinta en una máquina que ejecuta un agente de código: la seguridad y el aislamiento deben imponerse fuera de la cosa que arrancaste. En un portátil donde el agente y el navegador comparten un solo kernel, “fuera” no tiene dónde sentarse.

En un espacio de trabajo Bromure, el 8265 no está en tu Mac

Bromure Agentic Coding ejecuta cada espacio de trabajo en su propia VM Ubuntu sobre el framework Virtualization de Apple. Esa es la respuesta entera a este ataque, y aguanta sin que nadie parchee nada.

Cuando el agente ejecuta ray start --head, el panel se enlaza a 127.0.0.1:8265 dentro de la VM. La interfaz de loopback de tu Mac es otra interfaz. Un fetch() con rebinding desde Safari en el anfitrión resuelve evil.example a 127.0.0.1, conecta con el loopback de macOS y no obtiene nada, porque el servicio que la página venía a buscar está un kernel más allá. La cadena publicada apunta a 127.0.0.1, la dirección a la que singularity hace rebinding y la dirección que nombra el aviso, y en un espacio de trabajo Bromure esa dirección no tiene nada.

Lo mismo vale para la segunda mitad del aviso, la parte sobre usar un navegador como intermediario para alcanzar instancias de Ray dentro de una red corporativa que nadie puso en internet. Los espacios de trabajo funcionan en modo NAT por defecto, y el manual es específico sobre lo que eso compra: las VM son alcanzables desde tu Mac pero no quedan expuestas en tu LAN física, y las conexiones entrantes desde otro sitio no son posibles salvo que publiques un servicio a propósito. Publicar es un túnel rápido de Cloudflare por servicio que arrancas pulsando un botón.

Y te llevas el inventario. La tarjeta Puertos a la escucha del panel del espacio de trabajo consulta ss -tulnpH dentro del invitado y lista cada socket alcanzable desde fuera como el IP-de-la-VM:puerto al que te conectarías, con el nombre del proceso que lo tiene. La tarjeta oculta los sockets de solo loopback, ya que nada fuera de la VM puede alcanzarlos. La misma lista sale de bromure-cli vm <id> -L. Esa lista es el censo de los restos del agente, y macOS no lleva ninguna lista así por ti.

CONVENCIONAL: un solo loopback, compartido por todotu Mac · 127.0.0.1:8265 ray · :5173 vite · :8888 jupyter · :3000 mcp:8000 uvicorn · :5000 mlflow · :5432 postgresarrancados por el agente, sobreviven a la tarea, sin autenticaruna pestaña en otra ventanahace rebinding a 127.0.0.1y llega a todo estoBROMURE: el loopback del agente no es el tuyoVM del espacio de trabajo · su 127.0.0.1:8265 ray · :5173 vite · :8888 jupyterNAT: fuera de tu LAN físicasockets alcanzables listados en el paneltu Mac · 127.0.0.1(nada que arrancara el agente)la dirección que marca el exploitla misma pestañaconecta y encuentrauna interfaz vacía
El mismo comando, dos disposiciones. Convencionalmente, todo lo que el agente arranca se suma al montón que una página web puede direccionar. En Bromure los servicios del agente viven detrás de una frontera de red, y lo alcanzable es una lista que puedes leer.

El salto siguiente al primero

ShadowRay 2.0 merece leerse por lo que hace después un nodo comprometido, porque esa es la parte de la que el aislamiento tiene que responder. Escanea en busca de otras instancias de Ray y las infecta. Instala un cron job que recoge instrucciones nuevas cada quince minutos. Va a por las credenciales y los conjuntos de datos que haya en la máquina.

Cada una de esas cosas es una conexión saliente, y un espacio de trabajo Bromure contrasta las conexiones salientes con reglas que has escrito tú. El editor Conexiones salientes del panel Guardrails guarda un conjunto de reglas al estilo pf: permitir o denegar, por host, IP o CIDR, protocolo y puerto, evaluadas de arriba abajo con la primera coincidencia ganando, más un control para el tráfico que no coincide con nada.

allow web  api.github.com:443
allow tcp  registry.npmjs.org:443
deny  any  10.0.0.0/8
deny  any  192.168.0.0/16
default deny

Dos capas aplican ese mismo conjunto de reglas. El conmutador virtual evalúa cada flujo por IP de destino y por los hostnames que aprendió espiando las propias respuestas DNS del invitado, de modo que una regla escrita contra un nombre sigue coincidiendo cuando la dirección se mueve por debajo. El proxy MITM evalúa las mismas reglas por SNI de TLS, y para las reglas web por método HTTP, así que puedes escribir allow web api.example.com GET,HEAD. Bromure inyecta un reset en una conexión TCP denegada, de manera que el connect falla en lugar de quedarse colgado.

Toda esa aplicación se ejecuta en tu Mac, fuera de la VM. La interceptación transparente viene activada por defecto y desvía el HTTP y el HTTPS del invitado hacia Bromure incluso si algo de dentro borra HTTP_PROXY y HTTPS_PROXY. Un agente que leyó la página web equivocada, o una carga que entró con un paquete, queda del lado equivocado de las reglas que necesitaría editar. Bromure escribe cada decisión en Ventana → Línea de tiempo de seguridad como una fila de Cortafuegos: host, puerto, permitido o bloqueado, en una única tabla cronológica junto a las decisiones de credenciales, cadena de suministro e inyección de prompt.

El cron de quince minutos tiene el mismo problema en la otra dirección. Un espacio de trabajo son tres capas de almacenamiento, y dos de ellas son tuyas para tirarlas: Borrar home… devuelve /home/ubuntu a su estado posterior al clonado, Restablecer a la base… vuelve a clonar el disco de sistema del espacio de trabajo desde la imagen base, y la imagen base sigue siendo de solo lectura en ejecución. Una crontab en el directorio home no sobrevive a ninguna de las dos.

localhost era una convención

Los mantenedores de Ray no fueron descuidados. Escribieron un framework de cómputo que ejecuta comandos, lo dijeron en la documentación, y te dijeron que pusieras una frontera alrededor. La gente que lo ejecuta en un portátil tomó la interfaz de loopback por esa frontera, que es la conclusión a la que llevan invitando servidores de desarrollo, notebooks y paneles locales desde hace veinte años. Aquello era una convención, no un control. Aguantó mientras las únicas cosas en tu máquina eran cosas que arrancabas tú, y dejó de aguantar en cuanto los navegadores tuvieron un motor de scripts.

Un agente ensancha la brecha cada tarde, un npm run dev cada vez. El cuidado no la cierra, porque el cuidado exige saber qué está escuchando, y la lista cambia cada vez que el agente ejecuta un comando.

Deja de permitir que los dos compartan una interfaz. Pon el trabajo del agente en su propia máquina, donde los puertos que abre responden a tu Mac y a nadie más, donde los sockets alcanzables son una lista que lees en vez de una suposición que heredaste, y donde las conexiones que salen se contrastan con reglas que el espacio de trabajo no puede editar. Una página que hace rebinding a 127.0.0.1 se encuentra entonces una interfaz vacía y se lleva una negativa.


Fuentes: CISA, “CISA Adds One Known Exploited Vulnerability to Catalog” (17 de agosto de 2026) · The Hacker News, “CISA Flags Actively Exploited Ray Flaw That Can Trigger Browser-Based RCE” (18 de agosto de 2026) · Aviso de GitHub GHSA-q279-jhrf-cc6v (CVE-2025-62593) · Oligo Security, “ShadowRay 2.0”