OpenHands / OpenHands/software-agent-sdk

VSCode connection token is the agent-server session API key, and is exposed via URL and argv

Open
#4,317 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug priority:high security-related Stale
Dominant language
Python
Stars
1.1k
Forks
539
Avg merge
1d 19h
Merged PRs (30d)
137

Description

Summary

get_vscode_service() seeds the editor's connection token from config.session_api_keys[0], so the value that appears in the editor URL's ?tkn= query parameter is the same secret that authenticates every /api/* call on the agent server.

The service already knows how to mint an independent token — VSCodeService.start() generates os.urandom(32).hex() when connection_token is None — but that path is only reached when no session API keys are configured, which is not the normal deployment.

Filing this publicly because the analysis is already public: it came out of a code review on OpenHands/OpenHands#16106, and I could not find a SECURITY.md or a private reporting channel on this repo. Happy to move it if you'd prefer.

Where

openhands-agent-server/openhands/agent_server/vscode_service.py on main (and identically in the released openhands-agent-server==1.39.1 wheel):

# :247-249 — the token is the API key
connection_token = None
if config.session_api_keys:
    connection_token = config.session_api_keys[0]
# :54-55 — the independent token that only applies when there are no session keys
if self.connection_token is None:
    self.connection_token = os.urandom(32).hex()
# :120 — and it goes in the URL
return f"{base}/?tkn={self.connection_token}&folder={workspace_dir}"
Why it matters

The consequence of the editor token leaking is not "someone can read my editor" — it is full /api/* access on the agent server, i.e. arbitrary conversation creation and whatever those conversations can reach.

Because it is a URL query parameter, it leaks through the ordinary paths a URL does:

  • Browser history, bookmarks, session restore — stored in plaintext, and the editor URL is opened in a new tab by design.
  • Referer headers — the workbench renders webviews, Markdown previews and extension content from the document whose URL carries the token. Nothing in the URL-construction path sets a Referrer-Policy.
  • Access logs — any reverse proxy or ingress in front of the agent server logs the query string by default.

There is a second, independent exposure of the same secret. _start_vscode_process builds a shell command string and passes it to asyncio.create_subprocess_shell:

# :170-171, :179
f"--host 0.0.0.0 "
f"--connection-token {self.connection_token} "
...
self.process = await asyncio.create_subprocess_shell(cmd, ...)

Because the command execs, openvscode-server's own argv carries the token, so ps aux inside the container shows the agent server's API key to any process that can read the process table — including agent-run bash commands in that same container, which is the ordinary case rather than an exotic one.

--host 0.0.0.0 is worth noting alongside it: under docker run --network host the editor port is directly host-reachable with only this token in front of it.

Suggested fix

The smallest change is to stop seeding from the session key and let the existing random-token path run in all cases — delete the config.session_api_keys branch at :247-249 so connection_token stays None and start() generates one.

That looks safe from the consumer side: callers get the editor URL from GET /api/vscode/url rather than reconstructing it, so nothing outside this module needs to know the token's provenance. Worth confirming against the cloud exposed_urls path, which I can't see from here.

Separately, and independently of the above, it would be worth keeping the token out of argv — create_subprocess_exec with an argument list at minimum, and a token file if openvscode-server accepts one (VS Code's own server CLI exposes --connection-token-file; I have not verified it on the OpenVSCode build you pin).

Downstream context

OpenHands/OpenHands#16106 makes the editor reachable on self-hosted installs, which is what surfaced this. That PR mitigates what it can from the outside — it keeps the editor route off the credential-required public-mode origin and sends Referrer-Policy: no-referrer on the editor path — but neither of those addresses the token's scope, which only this repo can.

I'm happy to open a PR here for the one-line change if you agree with the direction.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in openhands-agent-server/openhands/agent_server/vscode_service.py, tracing get_vscode_service(), VSCodeService.start(), URL construction, and _start_vscode_process(). Confirm the cloud exposed_urls path and how callers consume GET /api/vscode/url. Done means the editor receives an independent token rather than a session API key, with token exposure in the subprocess launch path addressed or explicitly scoped.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
api, backend, security
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.