Skip to content

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

Description

@harish-chandramowli

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingpriority:highFor bugs, affecting nearly all users and degrading performance or UX.security-related

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions