Fixed in main on 29 September, Still Shipping on npm: Agent Canvas Publishes Its Own Session Key
On 3 October 2026, researchers filed issue #17878 against OpenHands/OpenHands, reporting that the Agent Canvas desktop client serves its full-privilege Session API Key in plaintext from an unauthenticated page that listens on every network interface. Any host on the same LAN can read the key with one GET and then run arbitrary commands on the victim’s machine.
That is a serious bug, and it is not the interesting part. The interesting part is that the project had already fixed it. Pull request #17007 — which binds the local launchers to loopback and refuses to inject the session key into off-loopback listeners — merged on 29 September 2026, four days before the report arrived. The researchers found a vulnerability that upstream had closed in main and never shipped.
The defect, as the reporters describe it
The report is credited to HuanChen (China Telecom DakeLab) and song (JHU), and it is unusually disciplined for an unsolicited issue. The mechanism it describes:
Agent Canvas starts four local services. The Agent Server (127.0.0.1:18000) and the Automation backend (127.0.0.1:18001) are correctly restricted to loopback. The two services in front of them — the static frontend server and the ingress reverse proxy on port 8000 — are not.
On the default startup path the client bakes a 32-byte Session API Key directly into the landing page HTML as window.__AGENT_CANVAS_SESSION_API_KEY__, so the user “never has to paste it.” That page requires no authentication. The reporters enumerate the consequence:
“Any LAN host can steal the Session API Key with a single unauthenticated
GET http://<victim>:8000/— and also directly via:3001, bypassing the ingress entirely.”
The stolen credential is not a scoped read token. Per the report it is identical to the Agent Server’s OH_SESSION_API_KEYS_0, and it is reused as the Automation administrator key and the KV-store encryption secret. It carries command execution (POST /api/bash/execute_bash_command), arbitrary absolute file reads, the global settings object, stored LLM and third-party credentials in plaintext, and the full conversation history. The reporters assign CVSS 3.1 8.8 (High), vector AV:A/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H — adjacent-network, no privileges, no user interaction.
They also pre-empt the obvious rebuttal, and the reasoning is worth preserving because it is the single most common misunderstanding in this bug class:
“CORS is a browser-internal same-origin policy: it constrains browsers only. … a LAN attacker using Python, curl, or any raw TCP client never consults the browser’s same-origin check at all.”
The control experiment in the report is the detail that makes it credible: the same command-execution request without the key returns HTTP 401. Authentication works. The defect is that the credential is published next to the lock.
This is the NeighborJack pattern applied to a desktop agent client, and it rhymes with Tugtainer’s fail-open agent secret: binding the backend to 127.0.0.1 buys nothing when the facade in front of it answers the whole subnet and hands out the key that unlocks the backend.
What we verified in the published package
We checked the code rather than the narrative. At tag v1.24.0 — the version npm currently serves as latest — scripts/static-server.mjs defaults to host: "::" and calls server.listen(config.port, config.host, …). In the same tag, scripts/ingress.mjs calls server.listen(config.port, …) with no host argument at all, which in Node means every interface. scripts/static-build.mjs passes VITE_SESSION_API_KEY: config.sessionApiKey into the build. The file scripts/bind-host.mjs does not exist at that tag; it returns HTTP 404 from the v1.24.0 tree and HTTP 200 from main.
The fix in main describes the vulnerability in its own header comment, which is as close to an upstream confirmation as you get without an advisory:
“Default is IPv4 loopback. Binding
::/0.0.0.0exposes the UI (and any session key injected into index.html) to every peer on the network.”
PR #17007 is a real fix and a broad one — 30 files, a new bind-host.mjs helper, a dedicated Playwright bind-policy config, and unit tests including docker-session-key-policy.test.ts and bind-host.test.ts. Its stated summary: bind npm/npx launchers to 127.0.0.1 by default, and “refuse to inject the session key when a launcher binds off-loopback, switching the UI to its API-key entry flow instead.”
The gap nobody owns
Here is the timeline that matters, all of it from the project’s own metadata:
- 25 September 2026 —
@openhands/agent-canvas1.24.0 published to npm,gitHead 7dc6805. - 29 September 2026 — PR #17007 merges to
mainasc5efa3a. The vulnerability is fixed in source. - 3 October 2026 — issue #17878 filed, independently reporting the same defect. Labelled
priority:highandready-for-dev. - 3 October 2026 — release PR #17717 (“chore(main): release 1.25.0”) remains open and unmerged.
We confirmed the npm state directly against the registry: dist-tags.latest is 1.24.0, published 2026-09-25T15:25:49Z, and the registry’s modified timestamp is the same date. There has been no release since. Every user who runs npx @openhands/agent-canvas today gets the pre-fix code, five days after the fix landed.
A commenter on the issue reached the same conclusion from the other direction, reporting that on macOS a default npx -y @openhands/agent-canvas --port <port> left the ingress listening on *:<port> and answering HTTP 200 on the machine’s Wi-Fi address. We did not reproduce that run ourselves and report it as a third-party claim; the static evidence above stands on its own.
Why the release lag is the actual vulnerability
There is no CVE for this, no GitHub Security Advisory, and therefore no OSV record and nothing for a scanner to match. We have written before about fixes that ship without advisories and leave npm audit reporting zero findings; this is the harder version of that problem, because here even a perfect advisory database would not help. The fix is in main and the registry has no artifact containing it. There is no “upgrade to” version to publish.
That inverts the usual security-engineering instinct. The normal failure mode is a known bug awaiting a patch. This is a patched bug awaiting a release, which produces a worse disclosure dynamic: the fix commit is public, it names the defect in a code comment, it ships test cases that demonstrate the policy, and the vulnerable artifact is the one the install command resolves. A merged security fix in a public repository is a disclosure whether or not anyone writes an advisory — and the window between that merge and the release is a window in which the exploit is documented and the patch is unobtainable.
The same structural lesson showed up in the four-month wait on MCP’s own fetch server, where a volunteer’s fix sat unmerged at CVE publication. Different direction, identical root cause: the repository and the registry are two different systems, and security posture is determined by whichever one your users actually install from.
What to do now
- Check what you are actually running.
npm view @openhands/agent-canvas version. If it is 1.24.0 or earlier, you have the pre-fix launcher regardless of whatmainlooks like. - Bind to loopback explicitly. Do not rely on the default. The post-fix code accepts
--hostandOH_BIND_HOST; on the released version, pass the host flag to the static server yourself rather than trusting::. - Treat the port as hostile until verified. From another machine on the same network, request the Agent Canvas port and grep the response for
SESSION_API_KEY. This takes one command and it answers the question definitively for your build, desktop or npx. - Firewall the agent host. Coffee-shop Wi-Fi, conference networks, and flat office VLANs are exactly the adjacent-network precondition this needs. A host firewall denying inbound on the agent ports removes the attack independently of the upstream release schedule.
- Rotate the LLM and provider credentials stored in the client if it has run unfirewalled on a shared network. The report’s claim is plaintext retrieval via
GET /api/settings/secrets/{name}; assume disclosure rather than proving absence. - For maintainers of any agent tool: watch your merge-to-release latency as a security metric. A security fix sitting in
mainbehind an open release PR is not a shipped fix.
Our verification was primary-source-led and static. We read issue #17878 and its comments, pull requests #17007 and #17717, and the #17007 file list through the GitHub API, confirming merge state, merge date 2026-09-29T00:47:28Z, and the 30 changed files. We retrieved scripts/static-server.mjs, scripts/ingress.mjs and scripts/static-build.mjs at tag v1.24.0 and confirmed the host: "::" default, the hostless server.listen in the ingress, and the VITE_SESSION_API_KEY build variable; we confirmed scripts/bind-host.mjs is absent at that tag and present on main, and quote its header comment as published. We queried the npm registry directly for @openhands/agent-canvas and confirmed latest = 1.24.0 published 2026-09-25. We did not install or run Agent Canvas, did not send any request to any third-party host, and did not attempt exploitation; severity figures and the runtime observations are the reporters’ own and are attributed as such. No CVE or GitHub Security Advisory existed for this issue at the time of writing.
Sources:
- OpenHands/OpenHands issue #17878 — “Agent Canvas Desktop Client — Unauthenticated Session Key Disclosure Leading to Remote Code Execution” (opened 3 October 2026, open,
priority:high/ready-for-dev) - OpenHands/OpenHands pull request #17007 — “fix: keep local session keys off externally reachable listeners” (merged 29 September 2026,
c5efa3a, 30 files) - OpenHands/OpenHands pull request #17717 — “chore(main): release 1.25.0” (open and unmerged at the time of writing)
- OpenHands/OpenHands —
scripts/bind-host.mjsonmain(loopback default; header comment describing the exposure) - OpenHands/OpenHands —
scripts/static-server.mjsat tagv1.24.0(host: "::"default) - npm registry —
@openhands/agent-canvas(latest= 1.24.0, published 25 September 2026)