Back to all posts
Published on · by Renaud Deraison

The web page called localhost

On August 17, CISA added Ray's CVE-2025-62593 to its Known Exploited Vulnerabilities catalog. It is remote code execution on a developer's own machine, delivered by a web page the developer visited, reaching a service that was only ever listening on 127.0.0.1. Coding agents have been filling that interface with unauthenticated servers all year. Bromure Agentic Coding puts them somewhere a browser tab cannot dial.

Your dev server, your notebook and your local dashboard each bind an unauthenticated port to 127.0.0.1, on the reasoning that only the person at the keyboard can reach it. A web page can reach it as well. The browser policy standing in the way comes apart in about sixty seconds.

On August 17, CISA added one vulnerability to its Known Exploited Vulnerabilities catalog and gave federal agencies until August 20 to deal with it. The Hacker News wrote it up the next morning. The entry is CVE-2025-62593, a code-injection flaw in Ray, the distributed compute framework a large share of the industry uses to train and serve models.

The code runs on a developer's own laptop. Someone had Ray running locally for their work, opened a web page, and the page ran a command for them.

Three ordinary facts, arranged badly

The advisory rates it 9.4 and describes a chain with no exotic step in it.

Ray's Jobs API takes a shell command. That is the product: you POST to /api/jobs/ with an entrypoint and the cluster runs it. The dashboard listens on port 8265, and neither it nor the job endpoints require authentication. That was a deliberate call by the maintainers, whose position is that security and isolation must be enforced outside the Ray cluster. You put it somewhere only trusted callers can reach, and a laptop's loopback interface sounds like such a place.

Ray did carry one guard against browsers. It checked whether the User-Agent header started with Mozilla and refused those requests, on the assumption that a browser cannot lie about its own User-Agent. In Firefox and Safari, the Fetch API lets a page set that header to whatever it likes.

That leaves same-origin policy, the one control standing between a page on the internet and a service on your machine. DNS rebinding takes it apart. The attacker serves the page from a domain whose DNS records they control, with a very short time-to-live. The page loads, then re-resolves that same domain to 127.0.0.1. The browser keeps treating it as the same origin, same hostname and same port, while every request now goes to your machine. The published proof of concept uses singularity, a rebinding framework that has been public for years.

Chain them and a malicious page, or a malvertisement on a page you trusted, posts a job to your Ray instance with a shell command in it. Chrome is protected by an unrelated bug; Firefox and Safari are not. Avi Lumelsky at Oligo theorized the User-Agent bypass; Jonathan Leitschuh, then at Socket, built the rebinding chain and the disclosure. Ray 2.52.0 fixes it and adds optional token authentication, off by default.

How a page becomes a client for your laptop1 · you load the pageevil.example → 203.0.113.7TTL: 1 second2 · the record changesevil.example → 127.0.0.1same origin, new destination3 · the guard is a headerfetch(url, headers: UA)Firefox and Safari allow it4 · POST /api/jobs/127.0.0.1:8265entrypoint = shellWHAT EACH LAYER WAS RELYING ONRay“the network around us is trusted”the User-Agent check“a browser cannot lie about this”same-origin policy“a hostname means one address”Only the third is a security control, and rebinding is older than most of the tools that lean on it.
The rebinding chain. Each step is a documented behaviour of a well-maintained tool: a short DNS TTL, a Fetch header the spec permits a page to set, and an API endpoint whose entire purpose is running commands.

Attackers got there before the advisory did. The RondoDox botnet picked the flaw up two days before public disclosure, and Oligo's ShadowRay 2.0 research tracks a campaign that turns Ray deployments into a self-propagating botnet: compromised clusters scan for more Ray instances and infect them, a cron job pulls fresh payloads from attacker-controlled repositories every fifteen minutes, and the payload mines Monero, opens reverse shells and takes what it finds. Oligo counted more than 230,000 exposed Ray servers, a tenfold increase since their first report in 2024. One compromised cluster gave up 240 GB of source code, models and datasets.

What is listening on your machine

Set Ray aside and count something else: the unauthenticated HTTP servers bound to your loopback interface right now, and how many of them you could name without looking.

Run lsof -iTCP -sTCP:LISTEN -P after a week of work and the list runs longer than you would guess. Vite on 5173. A FastAPI service under uvicorn --reload on 8000. Jupyter on 8888, mlflow ui on 5000, a Postgres container's published port, an MCP server on 3000, a debug port left open by a crashed test run. None of them ask who you are. Their authors made the assumption Ray's maintainers made, and it holds up until a tab in another window starts making requests.

Agents changed the volume. An agent working a ticket starts services as a matter of course. It runs the dev server to check its own change, brings up a database to run the migration, starts the notebook to look at the data, launches the API to hit an endpoint. It does this several times an afternoon, across several workspaces, and it does not clean up, because nothing tells it to. The processes outlive the task. By Thursday the interface carries a dozen services you did not start yourself and could not list from memory, each one binding without authentication because the framework's quickstart says that on localhost you are fine.

Ray's position reads differently on a machine running a coding agent: security and isolation must be enforced outside the thing you started. On a laptop where the agent and the browser share one kernel, “outside” has nowhere to sit.

In a Bromure workspace, 8265 is not on your Mac

Bromure Agentic Coding runs each workspace in its own Ubuntu VM on Apple's Virtualization framework. That is the whole answer to this attack, and it holds without anyone patching anything.

When the agent runs ray start --head, the dashboard binds to 127.0.0.1:8265 inside the VM. Your Mac's loopback interface is a different interface. A rebound fetch() from Safari on the host resolves evil.example to 127.0.0.1, connects to macOS loopback, and gets nothing, because the service the page came looking for is one kernel over. The published chain targets 127.0.0.1, the address singularity rebinds to and the address the advisory names, and in a Bromure workspace that address has nothing on it.

The same goes for the second half of the advisory, the part about using a browser as an intermediary to reach Ray instances inside a corporate network that no one put on the internet. Workspaces run in NAT mode by default, and the manual is specific about what that buys: the VMs are reachable from your Mac but are not exposed on your physical LAN, and inbound connections from elsewhere are not possible unless you publish a service on purpose. Publishing is a per-service Cloudflare quick tunnel you start by pressing a button.

And you get the inventory. The workspace dashboard's Listening Ports card polls ss -tulnpH inside the guest and lists every externally reachable socket as the VM-IP:port you would connect to, with the name of the process holding it. The card hides loopback-only sockets, since nothing outside the VM can reach them. The same list comes out of bromure-cli vm <id> -L. That list is the census of the agent's leftovers, and macOS keeps no such list for you.

CONVENTIONAL: one loopback, shared by everythingyour Mac · 127.0.0.1:8265 ray · :5173 vite · :8888 jupyter · :3000 mcp:8000 uvicorn · :5000 mlflow · :5432 postgresstarted by the agent, outliving the task, none authenticateda tab in another windowrebinds to 127.0.0.1and reaches all of itBROMURE: the agent’s loopback is not your loopbackworkspace VM · its own 127.0.0.1:8265 ray · :5173 vite · :8888 jupyterNAT: not on your physical LANreachable sockets listed in the dashboardyour Mac · 127.0.0.1(nothing the agent started)the address the exploit dialssame rebound tabconnects, finds anempty interface
The same command, two arrangements. Conventionally, everything the agent starts joins the pile a web page can address. In Bromure the agent's services live behind a network boundary, and what is reachable is a list you can read.

The hop after the first one

ShadowRay 2.0 is worth reading for what a compromised node does next, because that is the part isolation has to answer for. It scans for other Ray instances and infects them. It installs a cron job that fetches new instructions every fifteen minutes. It reaches for whatever credentials and datasets are on the machine.

Each of those is an outbound connection, and a Bromure workspace matches outbound connections against rules you wrote. The Outbound connections editor in the Guardrails pane holds a pf-style ruleset: allow or deny, by host, IP or CIDR, protocol and port, matched top to bottom with first match winning, plus one control for traffic that matches nothing.

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

Two layers enforce that one ruleset. The virtual switch evaluates every flow by destination IP and by the hostnames it learned from snooping the guest's own DNS answers, so a rule you wrote against a name still matches when the address moves underneath it. The MITM proxy evaluates the same rules by TLS SNI, and for web rules by HTTP method, so you can write allow web api.example.com GET,HEAD. Bromure injects a reset on a denied TCP connection, so the connect fails instead of hanging.

All of that enforcement runs on your Mac, outside the VM. Transparent interception is on by default and diverts the guest's HTTP and HTTPS to Bromure even if something inside unsets HTTP_PROXY and HTTPS_PROXY. An agent that read the wrong web page, or a payload that rode in with a package, sits on the wrong side of the rules it would need to edit. Bromure writes every decision to Window → Security Timeline as a Firewall row: host, port, allowed or blocked, in one chronological table alongside the credential, supply-chain and prompt-injection decisions.

The fifteen-minute cron has the same problem in the other direction. A workspace is three storage layers, and two of them are yours to discard: Erase home… resets /home/ubuntu to its post-clone state, Reset to base… re-clones the workspace system disk from the base image, and the base image stays read-only at runtime. A crontab in the home directory survives neither.

Localhost was a convention

Ray's maintainers were not careless. They wrote a compute framework that runs commands, said so in the docs, and told you to put a boundary around it. The people running it on a laptop took the loopback interface for that boundary, which is the conclusion dev servers, notebooks and local dashboards have invited for twenty years. That was a convention rather than a control. It held while the only things on your machine were things you started yourself, and it stopped holding once browsers got a scripting engine.

An agent widens the gap every afternoon, one npm run dev at a time. Care does not close it, because care requires knowing what is listening, and the list changes each time the agent runs a command.

Stop letting the two share an interface. Put the agent's work in its own machine, where the ports it opens answer to your Mac and to nothing else, where the reachable sockets are a list you read rather than an assumption you inherited, and where the connections going out are matched against rules the workspace cannot edit. A page that rebinds to 127.0.0.1 then finds an empty interface and takes a refusal.


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