Back to all posts
Published on · by Renaud Deraison

The sandbox opened the port

CVE-2026-65105 is a networking bug in NVIDIA NemoClaw, a tool whose whole purpose is running an AI agent inside a sandbox. The sandbox is a Docker container, containers cannot reach the host's loopback interface, so NemoClaw started the local model server on every interface instead — which switched off the one check that would have stopped a web page from talking to it. What a visiting page then took was the model's chat template, the layer that renders every system prompt before the model reads it. In Bromure Agentic Coding the local engine binds loopback on a port the guest never learns, and the guest reaches it over vsock instead of over a network.

The containment is what opened the hole. A sandbox that cannot reach your machine's loopback interface pushes the model server onto every interface instead, and then a browser tab can reach it too.

You are running your coding agent the careful way. It lives in a sandbox. The model it thinks with runs on your own machine, so no prompt leaves the building. You have done the two things everyone tells you to do.

Then you open a browser tab, and from that moment your agent starts writing code with a flaw in it that it declines to mention.

That is CVE-2026-65105, disclosed on August 25 by Elad Luz and Ofek Itach at Oasis Security, whose write-up calls it drive-by agent hijacking. They took it to NVIDIA's PSIRT before publishing, and the story ran the same day in The Hacker News, SiliconANGLE and CSO Online. NVIDIA has an advisory and a patch.

The product is NemoClaw, NVIDIA's tool for deploying an OpenClaw agent inside an OpenShell sandbox with a local Ollama backend for inference. Containment is the pitch.

The container could not reach loopback

Ollama listens on port 11434, and by default it binds 127.0.0.1 — the loopback interface, reachable only from the machine itself.

OpenShell sandboxes are Docker containers. A container has its own network namespace, so 127.0.0.1 inside the container is the container, not the host. The agent in the sandbox therefore cannot reach a model server bound to the host's loopback.

NemoClaw solved it in one environment variable. It launches Ollama with OLLAMA_HOST=0.0.0.0:11434: every interface on the machine, including the one facing your office wifi. The installer kept printing Using Ollama on localhost:11434 while the socket answered the world.

Then comes the step that turns a configuration choice into a vulnerability. Ollama's API has no authentication, so it carries two guards instead. The first is a CORS check on the Origin header. The second is a Host header allowlist that accepts localhost, the machine's own hostname, and names ending in .localhost, .local or .internal.

The second guard contains an exception. Ollama looks at the address it is bound to, and if that address is not loopback, it skips the Host check entirely. You can see why someone wrote that: an operator who binds a public interface wants remote clients, and a Host allowlist built for loopback would turn every one of them away. The result is that the setting which exposes the port is the same setting that disarms the check protecting it.

That leaves the CORS check, and DNS rebinding walks through CORS by construction. The attacker serves their page from a domain they control, on port 11434, then re-resolves that domain to 127.0.0.1. Origin and Host are now the same attacker-chosen hostname, which Ollama reads as same-origin. We covered the rebinding mechanism in detail last week, so take it as given: a page you visit becomes a client for a service on your machine, and it takes about a minute.

Rebinding is the interesting path in. It is also the harder one. A server on 0.0.0.0 answers every other device on the network segment with no browser trick at all: the guest laptop, the smart TV, the compromised machine two desks over.

the constraintOpenShell sandbox = a Docker containerits own network namespace, so127.0.0.1 = the container, not the hostthe agent cannot reach the model servera real problem, and it needed a real answerthe answer NemoClaw shippedOLLAMA_HOST=0.0.0.0:11434every interface, no authenticationinstaller prints: "Using Ollama on localhost"Ollama's two guards1 · CORS check on the Origin header2 · Host allowlist: localhost, hostname,.localhost .local .internalguard 2 is skipped when the bind is not loopbacktwo ways ina page: rebind the domain to 127.0.0.1, soOrigin and Host match and CORS passesthe LAN: dial the port directly, no trickboth arrive unauthenticated at the full APIThe configuration that exposed the port is the same configuration that turned off the check protecting it.
Why the port moved. The sandbox is a Docker container, so it cannot reach the host's loopback interface, and NemoClaw resolved that by binding the model server to every interface. Ollama skips its Host-header check whenever the bind address is not loopback, which leaves only a CORS check — and DNS rebinding makes Origin and Host the same attacker-chosen name.

They did not want code execution

Reaching the API is the boring half. What Oasis did once they had it is the new part.

The obvious move is to write a malicious system prompt into the model. That fails here: OpenClaw sends its own system prompt in the messages array on every request, and the client's prompt wins over the one stored in the model. So the researchers went one layer down.

Ollama's /api/create endpoint accepts a template field. A chat template is a Go template that turns the structured messages array — roles, contents, tool definitions — into the single flat block of text the model actually reads. It runs at inference time, after the client has handed over its messages, and it touches all of them.

The attack is three requests. Read the real template with /api/show. Keep its tool rendering and its special tokens exactly as they are, and change only how system messages get rendered, appending an instruction of your choosing. Write it back with /api/create.

The client still sends its own careful system prompt. The template wraps that prompt in the attacker's, every time, before the model sees a token of it. In Elad Luz's words, quoted by CSO Online, the instruction sits "one layer beneath anything a guardrail or an operator can see."

The edit lands in the model definition on disk, so it outlives the conversation, the reset and the restart. The model's metadata, size and listed capabilities stay the same, and nothing looks different in a list of installed models. Every consumer of that model gets it: the CLI you poke at by hand, and the agent running while you are at lunch.

Oasis lists what such an instruction is good for. None of it is dramatic enough to notice.

Write the bug in

Produce code with a subtle flaw in it — the kind that reads fine in review, because it was written to read fine in review.

Stay quiet

Never flag a security concern. An agent that used to warn you about something and no longer does is a hard thing to spot.

Steer the pick

Recommend a particular package, a particular URL, a particular configuration. You asked for a suggestion; you got one.

Send it home

Where the agent has network access, ship conversation contents and file contents to an endpoint of the attacker's choosing.

The rest of the unauthenticated API is a supporting cast: /api/generate and /api/chat to run inference on someone else's GPU, /api/pull to fill a disk, /api/delete to destroy the models, /api/push to publish under the victim's ollama.com account, and /api/me to learn the hostname, public key and signed-in username of whoever you just landed on.

The sandbox was never the blast radius

Oasis is careful about what the sandbox did and did not do. The OpenShell sandbox works: it isolates the filesystem, the network and the processes, and it keeps the endpoint clean. An agent inside an organization earns its keep by holding source control, CI/CD, internal APIs, cloud accounts, chat and a stack of MCP servers. Take the agent and you direct that access. The write-up's own summary is that the true blast radius follows the resources the agent is authorized to reach, not the boundary of its sandbox.

Which is the question a container never answers for you, and here it comes with a twist. The containment layer is what created the exposure. The Docker network namespace did its job. Reaching around it put the model server on the wifi.

Where the local model lives in a profile

Bromure Agentic Coding runs local inference too. A profile's agents can be pointed at an on-device model instead of a cloud provider, which is the same feature NemoClaw is offering. The plumbing underneath is built the opposite way round, and every step of it is a direct answer to something above.

The engine binds loopback, and never 0.0.0.0. The on-device model server runs on the Mac, on 127.0.0.1, on a port the kernel hands out at launch instead of a fixed number. The dynamic port is there for a dull reason: so Bromure never fights whatever already holds the traditional 11434 on that Mac, Ollama and LM Studio included. It also means there is no well-known port for a page to aim at.

The guest does not reach it over a network. Inside the VM, the agent dials 127.0.0.1:11434, the address it expects. That connection goes nowhere near a route. A bridge in the VM splices the TCP stream onto vsock, the virtio socket transport, on port 8446, and the host end of the channel hands it to the loopback engine. A vsock channel is a pipe between a hypervisor and one guest. It has no IP address, no hostname and no DNS record, so there is nothing to rebind, nothing to scan for, and no way for a browser to dial it. The guest never learns which port the engine is on.

Nothing on your LAN can reach the VM. Under Resources → Network, the default mode is NAT: egress works, and nothing on your local network can reach the VM. That closes the half of the NemoClaw finding which needs no rebinding at all, the device on the same wifi dialling the port directly, and it closes it without you changing a setting.

There is no model definition in the VM to rewrite. Guest agents are pinned to one model identifier, bromure-local, and they address it at bromure.llm, a synthetic hostname with no DNS record behind it. The in-VM proxy intercepts that name and forwards it to the host. Which model answers is a mapping you hold on the host, in the profile's Local Models panel, and changing it is a host-side remap: no reconfiguration inside the VM, no agent restart. The template, the weights and the routing all live on the Mac. Nothing in the guest holds a /api/create equivalent, because nothing in the guest holds the model.

Local inference runs the same pipeline as cloud. Because that traffic goes through Bromure's proxy rather than straight to a port, it gets the same treatment as a call to Anthropic or OpenAI: the same trace, the same logging, and the same prompt-injection detection. The Prompt Injection panel scores the file contents, web pages and tool output the agent reads with a local PromptGuard model, and scores the CLAUDE.md, AGENTS.md and GROK.md files it loads as authority with a fine-tuned ModernBERT classifier plus a deterministic scanner for invisible-Unicode payloads. Detections go to the Security Log, or pause the request, or return a hard 451, depending on which of the three responses you picked.

A port on every interfacewhere does the engine listen?0.0.0.0:11434 · fixed, well knownhow does the sandbox reach it?over IP, like everyone else on the wifiwho else can reach it?a rebinding page, and the LAN, unauthenticatedwho can rewrite the chat template?anything that can reach the port, viaPOST /api/createA channel with no addresswhere does the engine listen?127.0.0.1:<kernel-assigned> · host onlyhow does the VM reach it?vsock 8446 — a pipe, not a networkwho else can reach it?nobody: no IP, no hostname, no DNS recordwho picks the model?you, in the profile's Local Models panel;the guest only knows the name bromure-local
Two ways to give an agent a local model. On the left, an HTTP port on every interface, unauthenticated, with a writable model definition behind it — reachable by any device on the network and by any page that rebinds a hostname. On the right, a loopback engine on a kernel-assigned port, reached from the guest over a vsock channel that has no address to dial, with the model selection held on the host.

Now assume the model is lying to you

Take an architecture argument far enough and someone should test it. So hand the attacker the win by whatever route you prefer: a poisoned template, a bad day at a model provider, a prompt injection that got through. Your agent now works for someone else and its output still reads well. Walk those four payloads through a profile.

Steer the pick, and the pick still gets judged. This payload leans hardest on the model's authority, and it hits the most wall. Bromure evaluates every package fetch at the host proxy before the agent sees the response, across npm, PyPI, Cargo, RubyGems, Maven, NuGet, Go modules and Packagist, and the proxy does not ask who suggested the package. A two-day age gate is on by default, which handles most of what a freshly-published typosquat needs. Turn on the OSV lookup, point Package filtering at socket.dev or Delpi, and strip install scripts, and a recommendation has to survive four opinions the model has no say in. The .npmrc and pip.conf inside the VM can tighten those settings. They cannot loosen them.

Send it home, over a wire you control. Exfiltration needs egress. Guardrails → Outbound connections is a pf-style rule table — an action, a protocol (tcp, udp, web, any), a host or CIDR, a port list, and for web a list of HTTP methods — matched top to bottom, first match wins, with Unmatched traffic set to Allow or Deny. Set it to Deny, list the hosts your work needs, and the attacker's endpoint is not among them. Enforcement is at the virtual switch and the SNI layer on the host, so nothing the guest does to its own routing changes the verdict.

Write the bug in, and then try to ship it. Guardrails also carry a mode for each service the agent can reach: Block destructive or Read-only on GitHub, GitLab, Bitbucket, AWS, Kubernetes, DigitalOcean, container registries and each configured database endpoint. In read-only mode a git push is classified as a write and blocked; a fetch is always allowed. The backdoored commit stays in the VM.

And the access it inherits is fake. Oasis's blast-radius point is the one a profile spends most of its effort on. The credentials in the VM are decoys: brm_… placeholders as environment variables, a synthetic ~/.kube/config with throwaway client certificates, a fake base64 blob in ~/.docker/config.json, and AWS requests re-signed on the host so that anything bypassing the proxy gets InvalidSignatureException instead of access. Private SSH keys are never in the VM at all — the host signs through a per-profile agent. Switch on Require approval to use for a given credential and each substitution becomes a host dialog with a time-bounded grant: five minutes, an hour, the rest of the session. Folder sharing is capped at eight directories you pick by hand, so the rest of your Mac is not there to read.

And you can see all of it. The Security Log window (Window → Security Log…) is one host-side chronological table: package verdicts, every destination the VM opened and whether it was allowed or denied, credential swaps, injection detections. Guest code cannot edit it, because guest code cannot reach it. When the day ends, Erase home… and Reset to base… under Resources → Storage take the profile back to its post-clone state.

Turn it on

If you run NemoClaw, take NVIDIA's patch — it moves Ollama back to loopback and puts a token-gated proxy in front of anything that legitimately needs to leave it. The Hacker News notes the Windows and WSL path is not fully covered by that release, so check your platform against the advisory rather than assuming. Then go and find every other model server on your machine and ask it what address it is bound to. The answer is more often 0.0.0.0 than people expect, because containers keep asking for it.

In a profile, the settings worth two minutes are the usual short list. Guardrails → Outbound connections with Unmatched traffic on Deny. Credentials → Require approval to use on anything that can spend money or delete data. Supply Chain → OSV vulnerability check and socket.dev or Delpi filtering if you have a key, on top of the age gate that is already on. Prompt Injection → both detectors, so the traffic to your local model gets read as carefully as the traffic to a cloud one.

Keep running the model on your own machine. That choice buys you prompts which stay local, and it is worth having. It says nothing about who else gets to talk to the server holding them, which is a question about a socket, and a socket deserves a better answer than an environment variable set to make a container happy. Install Bromure Agentic Coding, pick your model on the host, and leave the guest with a pipe instead of a port.