Back to Blog
AI SecurityAI AgentsPythonCybersecurityVulnerability Management

OpenHands CVE-2026-112346: Who Can Run Commands in Your AI Agent Sandbox and How to Fix It

Before 1.52.0, the OpenHands SDK put its agent server on the network with no key. Who is exposed, how to check in minutes, and why Apptainer still needs a key.

P
Rafael Melo
October 30, 202611 min read

On 26 October 2026, a critical flaw was published in the OpenHands Software Agent SDK, the Python toolkit teams use to build AI coding agents that run commands, edit files and browse the web inside a sandbox. CVE-2026-112346 is rated 9.8 out of 10. Before version 1.52.0, the SDK's Docker launcher published the sandbox's agent server on every network interface of the machine, with authentication off by default. Anyone who could reach that port could run shell commands as root inside the sandbox, read and change the code mounted into it, and read the secrets handed to the agent.

The fix shipped on 5 October, three weeks before the CVE. As of 30 October there are no reports of the flaw being exploited, and it is not on the official list of vulnerabilities known to be exploited.

Two details make this more than a version bump. The release that fixed it changed only the Docker launcher: the Apptainer launcher, named in the same advisory, still starts the server on every interface in the current release. And on either launcher, the server still runs without a key unless you set one. Below: who is exposed, how to check in a few minutes, and what to change.

An Agent Server Is a Remote Shell by Design

An AI coding agent needs somewhere to act. The SDK starts an agent server inside a container, and the agent sends it HTTP and WebSocket requests: run this command, write this file, show the diff. Running commands is the server's job, so there is nothing clever to exploit. Whoever can talk to it can do whatever the agent can do.

What normally stands between that server and everyone else is a network boundary and a session key. Before 1.52.0, the Docker launcher removed both:

  • It started the container with -p PORT:8000, which Docker publishes on all of the host's interfaces, not only on localhost.
  • The server's list of accepted session keys was empty unless you set one, and an empty list means no authentication.
  • If you did set a key, the launcher forwarded it to the server but its own client dropped it, so the key broke your agent runs instead of protecting them.

An attacker who reaches the port gets:

  • A root shell in the sandbox, with whatever network access the sandbox has.
  • Your code. The workspace mounted into the sandbox is usually a checkout of your repository, readable and writable. A change planted there can end up in the commit or pull request your agent opens next.
  • The agent's secrets. Model provider API keys, git tokens and any other credentials passed to the agent sit inside the sandbox in plain text.
  • The editor port. With the launcher's extra ports option, a browser-based code editor on the next port up was published the same way.

Who Is Exposed

The advisory covers two launchers in the SDK's workspace package: DockerWorkspace (and DockerDevWorkspace, which inherits from it) before 1.52.0, and ApptainerWorkspace. You are exposed if you, or a tool you run, start agent sandboxes through them:

  • Custom agents and automation built on the SDK: a CI job that lets an agent fix failing tests or answer issues, an internal coding assistant, a benchmark or evaluation harness.
  • Developer laptops. On office, hotel or conference Wi-Fi, the port is open to everyone on that network for as long as the agent runs.
  • Cloud VMs and CI runners with public addresses. The port is open to the internet unless something in front of the machine blocks it.
  • Shared GPU and research servers. Apptainer is the usual container runtime on shared clusters where Docker is not allowed, which is exactly where other users sit on the same network.

Three things make the exposure wider than it looks:

  • Your host firewall may not apply. Docker writes its own packet-filtering rules for published ports, and on hosts managed with ufw those rules take precedence: a port Docker publishes is reachable even when ufw says it is closed.
  • The port changes every run. Unless you set one, the launcher picks a free port at startup, so a firewall rule for one port number does not cover it.
  • Parallel runs multiply it. A harness that starts many sandboxes at once opens one port per sandbox.

What 1.52.0 Changed, and What It Did Not

Version 1.52.0 changed two things in the Docker launcher: it publishes the ports on 127.0.0.1 only, and its client now sends the session key when one is set. We captured the command each version generates:

text
1.51.0  docker run ... -p 8010:8000 ghcr.io/openhands/agent-server:latest-python --host 0.0.0.0 --port 8000
1.52.0  docker run ... -p 127.0.0.1:8010:8000 ghcr.io/openhands/agent-server:latest-python --host 0.0.0.0 --port 8000

Inside the container the server still listens on all interfaces (Docker's port forwarding needs that) and still runs without authentication unless you set a key. On a fixed version it is no longer reachable from your network, but it is still reachable from the host itself and from other containers on the same Docker network.

The Apptainer launcher is unchanged from 1.51.0 to 1.59.0, the current release (28 October). It runs apptainer run ... --host 0.0.0.0 without a network namespace of its own, so the server shares the host's network and listens on every interface of the machine. On Apptainer, upgrading does not close this flaw. The session key in Step 4 does.

How Big This Is

  • The fix was public three weeks before the alert. The change was proposed in the open on 21 September, released in 1.52.0 on 5 October, and published as a CVE on 26 October. Teams that learn about vulnerabilities from CVE feeds heard nothing until then, and any project that pinned the SDK before 5 October is still on an exposed version.
  • A young, fast-moving package. The SDK has shipped ten releases since the fix, several of them in the same week. Its four packages, openhands-sdk, openhands-tools, openhands-workspace and openhands-agent-server, are released together with the same version number, and the workspace package that holds the launchers alone is downloaded more than 300,000 times a month.
  • Nothing to reverse-engineer. The server documents its own API. Once someone finds the port, using it takes a few HTTP requests and no skill.

The honest caveat: an attacker has to reach the port while a sandbox is running, and Docker sandboxes started this way are normally stopped and removed when the run ends. There are no known attacks as of 30 October. A long-running harness, a shared GPU node or a CI runner with a public address changes that arithmetic quickly.

Running AI agents against your own code or infrastructure? Request a free AI security assessment: we find every agent sandbox you run, test which ones answer from your network without a key, and review which secrets and repositories those agents can reach.

Step 1: Find the SDK and Its Sandboxes

Check the installed version where your agents run. The launchers live in the workspace package:

bash
pip show openhands-workspace

Below 1.52.0, the Docker launcher is exposed. On any version, the Apptainer launcher needs a key. In repositories, check the requirement files and lock files:

bash
grep -iE '^openhands-(sdk|tools|workspace|agent-server)' requirements*.txt
grep -i -A1 'name = "openhands-workspace"' poetry.lock uv.lock

Then look at what is running. The Docker launcher names its containers agent-server- followed by a random ID:

bash
docker ps --filter name=agent-server- --format '{{.Names}}  {{.Ports}}'
text
agent-server-366f3c63-9b3a-4ed2-b8aa-0df3f2340dae  0.0.0.0:36957->8000/tcp, [::]:36957->8000/tcp
agent-server-60a6ed9f-fbd7-4012-bd7b-648407e209cf  127.0.0.1:41233->8000/tcp

The first sandbox is published on every interface. The second is published on loopback only, which is what a fixed launcher produces. On machines that use Apptainer, look for the launcher's command line:

bash
ps -eo pid,args | grep '[a]pptainer run.*--host 0.0.0.0'

Step 2: Test It From Another Machine

From a different machine on the same network, ask the server something harmless:

bash
curl -s -o /dev/null -w '%{http_code}\n' http://HOST:PORT/api/conversations/count
  • 200: the server is reachable and needs no key. Treat it as exposed and read Step 5.
  • 401: reachable, but it refuses requests without the key.
  • Timeout or connection refused: not reachable from that machine.

Do not use /health for this test: it answers 200 whether a key is set or not. We checked all three cases against the agent server itself: the request returns 200 with no key configured, 401 once a key is set, and 200 again with the right key.

Step 3: Upgrade, Then Replace Running Sandboxes

Upgrade the four packages together, so they stay on the same version:

bash
pip install --upgrade openhands-sdk openhands-tools openhands-workspace openhands-agent-server

With uv or poetry, raise the floor to >=1.52.0 for all four and regenerate the lock file. Then rebuild any image or runner environment that installs them: the fix lives in the Python code on your side.

That also means pulling a newer agent-server image fixes nothing. The port mapping is written by the launcher when it creates the container, and Docker cannot change the mapping of a running container. Sandboxes started before the upgrade stay exposed until they stop, so stop the ones Step 1 showed on 0.0.0.0:

bash
docker stop agent-server-366f3c63-9b3a-4ed2-b8aa-0df3f2340dae

The launcher starts its containers with --rm, so a stopped sandbox is removed too.

Step 4: Set a Session Key on Every Launcher

The upgrade takes the Docker port off your network. A key makes the server refuse any request that does not carry it, which covers the host itself, other containers on the same network, and Apptainer, where the upgrade changes nothing. Set it in the environment of the process that starts your agent:

bash
export SESSION_API_KEY="$(openssl rand -hex 32)"
python run_agent.py

Both launchers forward the variable into the sandbox and send it with every request. Three details we confirmed in the code and in tests:

  • Use SESSION_API_KEY. The server also accepts a newer variable, OH_SESSION_API_KEYS_0, and prefers it when both are set, but the Apptainer launcher's client reads only SESSION_API_KEY. Set only the newer name and Apptainer runs a server that demands a key next to a client that never sends one.
  • Upgrade first on Docker. Before 1.52.0, the Docker launcher forwarded the key to the server and dropped it on the client side, so a key without the upgrade stops your agent runs instead of protecting them.
  • On shared machines, the key is visible. The Apptainer launcher passes the key on its command line, where other users of the same node can read it in the process list. Use a fresh key per run, and restrict inbound connections with the node's firewall: unlike Docker, Apptainer uses the host's network, so the host firewall does apply.

Step 5: If a Sandbox Was Reachable

Sandboxes started this way are deleted when they stop, and their logs go with them, so you will rarely be able to prove what happened. Rotate first, investigate second:

  • Rotate every secret the agent had: model provider API keys, git tokens, cloud credentials, and anything else in the agent's environment or secrets.
  • Review what the agents produced: commits, branches and pull requests from agent runs during the exposed period, with extra care for CI configuration, dependency files and scripts.
  • Check for activity you did not cause: spend on your model provider account you cannot explain, and git or cloud activity from addresses you do not recognize.
  • Look at the workspace on the host: files in the mounted directory that no run should have created.

The Real Problem

This flaw needed no clever attack: one default port mapping and one empty key list. It also shows how agent infrastructure is being treated. An agent sandbox holds a shell, your source code and a set of credentials, which is the risk profile of a CI runner. Yet it often runs on a laptop or a GPU box, started by a script nobody reviewed, with keys copied from someone's environment.

Give it the controls you would give a CI runner: authentication on every endpoint, no reachability from shared networks, credentials scoped to one run and expiring with it, and an inventory of where agents run. The same pattern, a dependency nobody chose to install sitting inside AI tooling, is what made the Authlib token flaws hard to find.

Free AI Security Assessment

The free assessment covers:

  • An inventory of your agent sandboxes: where the SDK and other agent runtimes run, on laptops, CI runners and GPU servers, and on which versions
  • A reachability test: which sandboxes answer from your network, and whether they demand a key
  • A review of what your agents can touch: the secrets, repositories and cloud permissions they get, and how to scope them to a single run

You get a written list of what is exposed and what to do about it, with no obligation attached.

Request your free AI security assessment, or read more about our AI security work, how we build AI agents with least-privilege access, and our DevSecOps practice.