Neutron, our AI engine, scored 97.1% on UC Berkeley's CyberGym benchmark. Learn more

Deep Dive

Deep Dive

OpenHands Software Agent SDK Unauthenticated Sandbox RCE (CVE-2026-112346)

The OpenHands Software Agent SDK exposes its sandbox agent port on every interface with auth off by default, allowing root commands; 1.52.0 fixes Docker but not Apptainer.

TL;DR

  • The OpenHands Software Agent SDK runs an agent in a workspace. Its agent server is the HTTP and WebSocket service that executes commands and file operations; a workspace launcher, DockerWorkspace or ApptainerWorkspace, starts that service. The OpenHands Software Agent SDK's Docker launcher publishes the sandbox agent server's port on every host interface, and its Apptainer launcher binds the server to 0.0.0.0 directly on the host. An empty session API key list disables authentication by default. Any host that can reach the port can send unauthenticated HTTP or WebSocket requests that run arbitrary shell commands as root inside the sandbox. This includes a container on a shared Docker network, a machine on the LAN, or an internet client when the host is exposed.
  • Every openhands-sdk release before 1.52.0 is affected. The package recorded 604,050 downloads in the last week on PyPI at the time of writing.
  • VulnCheck, which assigned and published the CVE, rates it Critical (CVSS v3.1 9.8, CVSS v4.0 9.3) under CWE-1327 ("Binding to an Unrestricted IP Address") and credits mil4n. No exploitation is known and no public proof of concept exists.
  • For Docker, update to openhands-sdk 1.52.0 or later, released October 5, 2026, recreate every sandbox container, and confirm the port now publishes on 127.0.0.1 only. For older deployments, block network access to the port.
  • Apptainer gets no change from 1.52.0, which still binds 0.0.0.0: set SESSION_API_KEY (leave OH_SESSION_API_KEYS_0 unset, or set both to the same value) and keep the port off untrusted networks. On pre-1.52.0 Docker, setting a key protects the server but breaks the SDK session; upgrade to restore authenticated use.

Am I affected?

You are affected if you run openhands-sdk with the DockerWorkspace launcher below 1.52.0 or with the ApptainerWorkspace launcher, no session API key is set, and the sandbox port is reachable from a network you do not fully trust. The advisory's affected range ends before 1.52.0, but the Apptainer launcher still binds 0.0.0.0 in 1.52.0. All three must hold:

  • Your code or platform uses the OpenHands Software Agent SDK, package openhands-sdk, and drives it with the DockerWorkspace launcher at a version older than 1.52.0, or with the ApptainerWorkspace launcher, which 1.52.0 leaves unchanged.
  • No session API key is configured, which is the default: neither SESSION_API_KEY nor OH_SESSION_API_KEYS_0 is set in the environment the SDK launches from, so the server's session_api_keys list is empty and authentication is off.
  • The published port is reachable by anyone you do not fully trust. The pre-fix launcher publishes it on every host interface, so this includes other containers on a shared Docker bridge network, anything on the host's LAN, and the open internet if the host has a public address and no blocking firewall.
Package Affected versions Fixed version
openhands-sdk (PyPI) before 1.52.0 1.52.0, released October 5, 2026
OpenHands Software Agent SDK DockerWorkspace launcher before 1.52.0 1.52.0: publishes on 127.0.0.1, and its client reads the configured session key
OpenHands Software Agent SDK ApptainerWorkspace launcher before 1.52.0 per the advisory, and 1.52.0 as well 1.52.0 changes Docker publishing only; Apptainer still binds 0.0.0.0 in v1.52.0. Configure a session key and isolate the port (see fix section).

Check what you run and where the port landed:

python -c "from importlib.metadata import version; print(version('openhands-sdk'))" 2>/dev/null || echo 'openhands-sdk not installed in this interpreter'
docker ps --format '{{.Names}}  {{.Ports}}' | grep -E 'agent-server|->8000/tcp'
# Run where the SDK starts; report presence without printing secret values.
python - <<'PYKEYS'
import os
for name in ("SESSION_API_KEY", "OH_SESSION_API_KEYS_0"):
    print(name + ": " + ("set" if os.environ.get(name) else "unset or empty"))
PYKEYS

In the docker ps output, a pre-fix sandbox shows a publish line like 0.0.0.0:31245->8000/tcp, meaning every interface, whereas the fixed launcher shows 127.0.0.1:31245->8000/tcp. The launcher's Docker run command names each container agent-server-<uuid>.

How do I fix it?

DockerWorkspace: update the SDK, then recreate every sandbox container, because the running container keeps its old port mapping until it is recreated:

python -m pip install --upgrade 'openhands-sdk>=1.52.0'

Then end each running sandbox session and start a fresh one, so the updated launcher creates a new container with the new port mapping (a plain docker restart keeps the old mapping), and confirm the new publish address with docker ps. The fixed Docker launcher's client reads OH_SESSION_API_KEYS_0 from the SDK process environment, falling back to SESSION_API_KEY. This matches the server's key selection, so the SDK's requests carry the configured key.

ApptainerWorkspace: 1.52.0 does not change the launcher's 0.0.0.0 bind, so upgrading alone leaves the port exposed. Configure the session key and isolate the port as described below.

If you cannot update yet, or you use Apptainer, restrict network access and configure authentication. The launcher-specific limits are:

  • Block the port: A host firewall rule or a Docker DOCKER-USER chain rule that drops non-local traffic to the sandbox ports closes the remote path, but the port remains bound on all interfaces, and any process or container that can still reach it has the full unauthenticated API.
  • Configure authentication: Run export SESSION_API_KEY="$(openssl rand -hex 32)" in the environment the SDK process starts from, before it starts; the launcher forwards the key and the server enforces it. A sandbox session that is already running keeps its key-less server, so start a fresh one.
  • Apptainer: Its client reads SESSION_API_KEY, including in v1.52.0. Set that variable and leave OH_SESSION_API_KEYS_0 unset, or set both to the same value, so the client and server agree.
  • Docker before 1.52.0: The client hard-codes its key to None. Setting either key variable protects the server but breaks the SDK session; use this as temporary containment and upgrade to restore the session.

What is the root cause?

The launcher published the sandbox port on every host interface and configured no session key, while explicitly overriding the agent server's own safe default of binding loopback when no key is set.

The vulnerable code is openhands-workspace/openhands/workspace/docker/workspace.py in the SDK at version 1.51.0. The launcher builds the Docker run command that starts the sandbox and its agent server:

ports = ["-p", f"{self.host_port}:8000"]        # line 218
if self.extra_ports:
    ports += [
        "-p",
        f"{self.host_port + 1}:8001",  # VSCode   # line 222
    ]

Docker's -p flag publishes a container port on the host. Without a host IP in the specification, as here, the publish address is 0.0.0.0: every interface the host has. With extra_ports enabled, which is off by default, the launcher also published the VS Code port 8001 at host_port + 1 with no host IP, so that second port was on every interface as well. Further down the same run command, the launcher passes the agent server its listening address explicitly:

run_cmd = [
    "docker", "run", "-d", "--platform", self.platform, "--rm",
    ...
    image,
    "--host",
    "0.0.0.0",                                    # line 248-250
    "--port",
    "8000",
]

Inside the container, binding 0.0.0.0 is what port forwarding needs. The exposure is decided by where the host-side publish lands, and that is all interfaces. Authentication is the second half. After the container starts, the launcher configures the SDK's own client for the workspace:

if not self.host:
    object.__setattr__(self, "host", f"http://127.0.0.1:{self.host_port}")
object.__setattr__(self, "api_key", None)          # line 272

Client key: The Docker client key is hard-coded to None. Separately, the launcher forwards any session key environment variables into the container, where only the server reads them; the SDK's own client never picks up the key.

Forwarded configuration: The Docker launcher's own forward_env field description explains that these variables let the server authenticate requests when it listens on 0.0.0.0.

Server default: Without a configured key variable, the server's session_api_keys list is empty. The CVE record identifies this default as disabling authentication.

The agent server's safe bind default was introduced in July 2026: pull request #4180, "fix(agent-server): default bind host to loopback without a session API key", made an unkeyed server bind loopback. The launcher's explicit --host 0.0.0.0 overrides that default, which is why the fix had to change the launcher rather than the server.

How is it exploited?

Any client that reaches the port can run shell commands as root in the sandbox with ordinary, unauthenticated API requests. No exploit code is public. To test exposure without a payload, run curl -s -o /dev/null -w '%{http_code}\n' http://<sandbox-host>:<port>/api/conversations/count from the network position you want to check: 200 means the API answers without a key, 401 means a session key is enforced, and 000 means the port is not reachable from there. The attack path from the CVE record and the source needs four steps:

  1. The attacker can reach the host on the published port: for example a container on the same Docker bridge network as the sandbox host, a host on the LAN, or any internet host if the machine is exposed and unfiltered.
  2. The attacker sends an HTTP or WebSocket request to the agent server's API. No key is required, because the server was started with an empty session_api_keys list.
  3. The server runs the requested shell command as root inside the sandbox container and returns whatever the command prints.
  4. Beyond command execution, the same API reaches the mounted workspace directory and the secrets store, and the CVE record states the secrets are read as plaintext.

Nothing in the chain needs a vulnerability in the request itself: the API is the SDK's own, designed for the agent to use, and it is simply listening for anyone.

How does the attack flow?

An unauthenticated request to a wrongly published port becomes root command execution inside the agent's own sandbox, with the workspace and secrets store attached.

flowchart LR A["Attacker on the network
LAN, container, internet"] --> B["Agent port reachable from the network
Docker: -p without a host IP
Apptainer: --host 0.0.0.0"] B --> C["Agent server with empty
session_api_keys list"] C --> D["Unauthenticated HTTP or
WebSocket request accepted"] D --> E["Shell command executes
as root in the sandbox"] E --> F["Workspace files and plaintext
secrets store read"] style B fill:#f8d7da style C fill:#f8d7da

What can an attacker do?

The blast radius is the sandbox container and the data mounted into it. Per the CVE record, an attacker can run arbitrary shell commands as root inside the sandbox container, access the mounted workspace the agent operates on, and read plaintext secrets from the secrets store the SDK provisions for the session. That is the agent's working set: the repository or directory it edits, the credentials in its secrets store, and anything else the session mounted. Two conditions gate it:

  • Network reachability of the published port, which the pre-fix launcher gives to every interface; no authentication, no user interaction, and no privilege beyond a network position.
  • A deployment that never configured a session key, which is the SDK's default.

Why does the fix work?

The fix is commit 7b66584, merged as pull request #5209 ("fix(workspace): publish Docker ports on loopback") on October 3, 2026. It was released in v1.52.0 on October 5 and addresses both the Docker publish address and the client key lookup:

-        ports = ["-p", f"{self.host_port}:8000"]
+        ports = ["-p", f"127.0.0.1:{self.host_port}:8000"]
         if self.extra_ports:
             ports += [
                 "-p",
-                f"{self.host_port + 1}:8001",  # VSCode
+                f"127.0.0.1:{self.host_port + 1}:8001",  # VSCode
-        object.__setattr__(self, "api_key", None)
+        session_api_key = os.environ.get(
+            "OH_SESSION_API_KEYS_0", os.environ.get("SESSION_API_KEY")
+        )
+        object.__setattr__(self, "api_key", session_api_key)

The publish now lands on loopback only, so no other host on the network can reach the port, and the VS Code port follows the same rule. That is the trade-off: a client on another machine can no longer connect to the published ports directly. The pull request presents the change as keeping the default networking local to the machine, since the SDK's own client already connects through 127.0.0.1. The server inside the container still binds 0.0.0.0, which the fix's own regression test asserts: the container-internal bind is what Docker's forward needs, and the host-side publish address is the boundary.

The client now resolves the same key the server accepts, preferring the current OH_SESSION_API_KEYS_0 variable with the legacy SESSION_API_KEY as fallback, so a configured key authenticates both directions instead of only the server's inbound checks.

The fix changes the Docker launcher, and its tests cover loopback publishing and client key lookup. The advisory also names Apptainer, but its launcher still passes --host 0.0.0.0 in v1.52.0.

For Apptainer, the fix changes nothing: the launcher runs the agent server directly on the host with --host 0.0.0.0, in 1.52.0 as before, so the port listens on every interface. The containment is the one in the fix section, a session key plus network isolation. The server listens at the workspace's host_port, a free port between 30000 and 39999 unless you set one. Run ss -ltn | grep ':<port> ' on the host to see whether it listens on 0.0.0.0, then nc -vz <host> <port> from a machine that should not reach it: a successful connection means the port is exposed and only the firewall and the key protect it.

How do I check if I was compromised?

No indicators of compromise have been published for this flaw, and no exploitation is known, so there is no IOC list to grep for. What you can check on a deployment that ran a pre-1.52.0 sandbox with a reachable port:

  • The port mapping (docker ps --format '{{.Names}} {{.Ports}}'): a pre-fix sandbox shows 0.0.0.0: in the publish line, which confirms the port was published on every interface. That is half of the exposure condition: your firewall rules and network path decide whether anything untrusted could reach it.
  • The sandbox container's logs for HTTP or WebSocket requests that do not correspond to your agent sessions' activity, since the agent server logs the requests it serves.
  • Your secrets store contents for anything the sandbox could reach: treat every secret provisioned to an exposed sandbox as disclosed, rotate them, and rebuild the container from a clean image.

If the exposure condition never held, because the port was firewall-blocked or the host had no untrusted network path, the practical risk was close to none, and patching on schedule is enough.

Timeline

  • 2026-07-23: the agent server gains its safe default, binding loopback when no session API key is set, in pull request #4180.
  • 2026-10-03: pull request #5209, "fix(workspace): publish Docker ports on loopback", merges as commit 7b66584.
  • 2026-10-05: v1.52.0 is released; its changelog lists the fix by its title with no security framing, and no advisory is published.
  • 2026-10-26: VulnCheck publishes CVE-2026-112346, CVSS v3.1 9.8 and v4.0 9.3, CWE-1327, crediting mil4n, with the affected range before 1.52.0.
  • 2026-10-26: the GitHub advisory database syncs the record as GHSA-rp7h-crm7-h5qj.
  • 2026-10-27: this post.

FAQ

Does the main OpenHands platform share the flaw?

The advisory covers the SDK's DockerWorkspace and ApptainerWorkspace launchers, which the SDK's local sessions use. Whether the hosted OpenHands platform or another product embeds these launchers is not established in the advisory, so operators of any OpenHands-based deployment should run the same checks: the installed SDK version and the publish address of any agent-server container. The pattern to look for is a container named agent-server-* publishing port 8000 on 0.0.0.0.

Why did v1.52.0 ship without a security advisory?

The changelog of v1.52.0 lists the pull request by its title, "fix(workspace): publish Docker ports on loopback", with no security framing, and no advisory accompanied the release. That is why dependency scanners had nothing to alert on until VulnCheck's CVE record appeared on October 26, and why version checks, not CVE feeds alone, were the reliable signal in between.

Is a session key alone enough protection?

It closes the unauthenticated command path: the server checks the session key on protected routes for conversations, command execution and file access. Public server-information routes are not a reliable authentication test. It does not move the port: a pre-1.52.0 Docker launcher still publishes on every interface and its own client cannot send the key, so the session breaks. A pre-1.52.0 Apptainer launcher keeps working with SESSION_API_KEY set, since its client reads it. For Docker, upgrading adds loopback publishing. For Apptainer, keep the port network-isolated as well as authenticated; 1.52.0 does not change its bind address.

Do I need to recreate running containers?

Yes. The port mapping is set when the container is created, so an already-running sandbox keeps its 0.0.0.0 publish even after you upgrade the SDK on the host. Stop and recreate every sandbox, then confirm the new mapping with docker ps.

Why was there no advisory for three weeks after the fix?

The CVE record, the GHSA entry and the database mappings all date from October 26, three weeks after the fixed release. Deployments that upgraded promptly were safe without knowing why, and deployments that pin versions had no advisory to alert on in the interim. This is the standard argument for version hygiene on agent infrastructure: sandbox components that bind network ports are worth tracking at release level, not only at CVE level.

Where is openhands-sdk still below 1.52.0?

Run Ostorlab SCA on your repositories and container images to list every project and release that still ships openhands-sdk below 1.52.0.

References

  • CVE-2026-112346 record, published October 26, 2026 by VulnCheck as the CNA, with the CVSS v3.1 9.8 and v4.0 9.3 vectors, CWE-1327, the affected range before 1.52.0 and the description of both launchers: https://www.cve.org/CVERecord?id=CVE-2026-112346 (checked October 28, 2026)
  • GHSA-rp7h-crm7-h5qj, the GitHub advisory database entry synced from the CVE record: https://github.com/advisories/GHSA-rp7h-crm7-h5qj (checked October 27, 2026)
  • VulnCheck advisory, "OpenHands Software Agent SDK < 1.52.0 Unauthenticated Sandbox Agent Server RCE", severity Critical, credit mil4n: https://www.vulncheck.com/advisories/openhands-software-agent-sdk-unauthenticated-sandbox-agent-server-rce (checked October 27, 2026)
  • Fix pull request #5209, "fix(workspace): publish Docker ports on loopback", merged October 3, 2026, with the client key propagation rationale in its description: https://github.com/OpenHands/software-agent-sdk/pull/5209 (checked October 27, 2026)
  • Fix commit 7b66584ff8899144eb4ce679c2811861342af9de, the loopback publish and session key resolution diff: https://github.com/OpenHands/software-agent-sdk/commit/7b66584ff8899144eb4ce679c2811861342af9de (checked October 27, 2026)
  • v1.52.0 release, published October 5, 2026, listing the fix without security framing: https://github.com/OpenHands/software-agent-sdk/releases/tag/v1.52.0 (checked October 27, 2026)
  • Vulnerable source at v1.51.0, openhands-workspace/openhands/workspace/docker/workspace.py, lines 89 to 95 (forward_env), 218, 222, 248 to 250 and 272: https://github.com/OpenHands/software-agent-sdk/blob/v1.51.0/openhands-workspace/openhands/workspace/docker/workspace.py (checked October 27, 2026)
  • Agent-server safe default, pull request #4180, "fix(agent-server): default bind host to loopback without a session API key", merged July 23, 2026: https://github.com/OpenHands/software-agent-sdk/pull/4180 (checked October 27, 2026)
  • PyPI download statistics for openhands-sdk, 604,050 downloads in the week checked: https://pypistats.org/packages/openhands-sdk (checked October 27, 2026)
  • Apptainer launcher at v1.52.0, retaining the 0.0.0.0 bind and SESSION_API_KEY client lookup: https://github.com/OpenHands/software-agent-sdk/blob/v1.52.0/openhands-workspace/openhands/workspace/apptainer/workspace.py (checked October 28, 2026)