Open feature request

Security review: verb exposure on port 8821 (core install on every container)

John Lauer · 2mo ago

adom-vscode ships as a core feature to every Adom user, so its HTTP verb surface needs a deliberate security review. Requested by John, 2026-08-01, alongside the 1.1.12 change that made exec HD-local only.

Done in 1.1.12:

  • /exec and /exec/stream are refused on cloud (hw) containers (errorCode exec_disabled_on_cloud, HTTP 403). HD workspaces sit behind the user's firewall; cloud containers do not. /health and /container report execAllowed, and the verb roster omits exec where disabled. Escape hatch: ADOM_VSCODE_ALLOW_EXEC=1 in the extension host env (platform-set only).

Still to consider (the future TODO):

  1. /command executes ANY VS Code command, including terminal sendText-style flows, so it is close to exec-equivalent. Decide whether it needs the same hd/hw gating or an allowlist of safe commands on hw.
  2. Other state-changing verbs on hw: apikey set (writes the session key via sudo), config set, extensions install/update, queue clear. Decide the per-verb policy matrix for hd vs hw.
  3. CORS is currently Access-Control-Allow-Origin: * on 127.0.0.1:8821. Credentialed cross-site requests are blocked by the browser under *, but tighten this anyway (drop CORS entirely or allowlist the workbench origin).
  4. Reachability: the code-server proxy (VSCODE_PROXY_URI /proxy/8821/) can expose the port beyond loopback with the user's auth. Audit what the proxy allows and whether :8821 should refuse proxied requests (e.g. require a loopback Host header or a shared token).
  5. Auth model: today the API trusts anything on loopback, like the pre-token adom-desktop bridges. Consider a bridge-token pattern (file readable only by the workspace user, required header) so a compromised web page or other-user process cannot drive the API. This is the ssh/sshd analogy: loopback trust was fine for one user, a token is the keypair.
  6. Caller identity: adopt X-Adom-Caller-* style attribution (like adom-desktop) so the Activity/queue records WHICH AI thread asked, for user visibility.

The bar: every verb should be justified per container class, and anything arbitrary-execution-shaped defaults to HD-local.

2 Replies

John Lauer · 8d ago

Found and fixed a real hole while reviewing this: adom-vscode 1.1.116 (published).

The hole. On a local Hydrogen workspace, WSL2 forwards :8821 to Windows localhost, so every web page in every browser on the PC can reach it. Combined with Access-Control-Allow-Origin: * (item 3), a page could read the answers (open tabs, and since 1.1.115 GET /file) and pass the CORS preflight for a JSON POST /exec. Measured from Windows on a test PC before the fix: 200, ACAO: *, execAllowed: true. Chrome and Edge now ask before a public site reaches localhost; Firefox does not, and DNS rebinding sidesteps that check.

The fix (items 3 and 4):

  • A request carrying an Origin header is served only when that origin is loopback (the workbench at localhost, *.localhost) or Tauri (Hydrogen's app). Every other origin, and the opaque null origin of sandboxed frames, gets 403 origin_not_allowed. Requests without Origin (curl, the CLI, Hydrogen's Rust side) are unchanged.
  • On a local workspace the Host header must be loopback too, which stops DNS rebinding.
  • On a cloud container the port is reachable only through code-server's signed-in proxy. I measured that the proxy forwards the workbench's Origin and Host unchanged, so there the workbench's own origin (Origin host equals Host) passes.
  • CORS now reflects the allowed origin (with Vary: Origin) instead of *, including the tab-events stream.

Verified from Windows on the test PC, 1.1.116:

caller result
no Origin (curl, CLI) 200
Hydrogen's app (http://tauri.localhost) 200
the workbench through code-server's proxy (GET and POST) 200
hostile site, GET /file 403
hostile site, preflight for /exec 403
Origin: null 403
DNS rebinding (Host: evil.example) 403
lookalike http://localhost.evil.example 403

Hydrogen's full regression sweep (217 checks) passed on that PC with 1.1.116 installed, so Hydrogen's own calls into :8821 are unaffected. The macOS line (adom/adom-vscode-macos) is a separate package and is not changed by this.

Still open here:

  • 5, a token: now defense in depth only; the origin and host checks close the cross-site path. One residual: a page shown through Hydrogen's own webview proxy runs at 127.0.0.1:47084, a loopback origin, so it would pass. A token would close that.
  • 6, caller identity (X-Adom-Caller-*) for the Activity view: not started.
  • 1 and 2 (/command and the state-changing verbs on cloud): with web pages shut out, on cloud these are reachable only by the signed-in owner through code-server, the same person who has the terminal. I'd leave them as they are unless you want an allowlist anyway.
John Lauer · 2d ago

Review of what is left after 1.1.116 (items 3 and 4 fixed), with the current code (1.1.117):

1. /command on cloud containers: no extra gate needed. On a cloud (hw) container, :8821 is reachable two ways: through code-server's proxy, which requires the user's signed-in session, and from processes inside the container. Both callers can already run anything. The signed-in session opens a terminal in the same editor, and a process in the container is already running code as that user. /command (even a terminal sendSequence) gives neither of them anything new. The origin gate from 1.1.116 blocks the one caller that would gain something: a web page in someone's browser. So /command stays ungated. /exec stays refused on hw only as defense in depth.

2. The other state-changing verbs on hw (apikey set, config set, extension install and update, queue clear): same reasoning, same callers, nothing gained. No per-verb matrix is needed while the trust boundary is the signed-in session plus the container user.

5. A token: still worth doing, for one case. On a local Hydrogen workspace, WSL forwards :8821 to Windows localhost for every Windows account on that PC, not just the owner. On a shared PC, another Windows user's process could drive this workspace's editor. A token file in the workspace user's home, read by Hydrogen through \\wsl$ (Hydrogen already reads port.json that way) and required as a header on state-changing verbs, closes that. It is low risk (shared Windows PCs are rare among users) but cheap. This needs a Hydrogen change and a CLI release together.

6. Caller attribution: not done. The extension keeps no request log today. Plan: the CLI and Hydrogen send X-Adom-Caller-Thread and X-Adom-Caller-App, and the extension keeps the last 200 state-changing requests (time, verb, caller) at GET /activity, so a person can see which AI thread drove their editor.

Leaving this open for 5 and 6. Items 1 to 4 are settled.

Log in to reply.