OpenHands / OpenHands/software-agent-sdk
VSCode connection token is the agent-server session API key, and is exposed via URL and argv
Nobody has claimed this yet.
- 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.
Refererheaders — the workbench renders webviews, Markdown previews and extension content from the document whose URL carries the token. Nothing in the URL-construction path sets aReferrer-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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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