Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Remote Session UI Security Proposal

The current Linux remote-session-ui bridge in tools/remote-session-client/src/bin/remote_session_ui.rs is a trusted local web bridge: a loopback HTTP listener whose Rust backend owns the TCP connection to the capOS gateway and the upstream session, while browser JavaScript receives only DTOs (view models, call results, denial diagnostics, and redacted transcript rows). This document describes the web-security posture required before that bridge ships beyond research use, and how the planned Tauri desktop wrapper inherits the controls. It is the design note referenced from REVIEW_FINDINGS.md and cross-linked from docs/proposals/security-and-verification-proposal.md.

Threat Model

The bridge holds the operator’s authority to drive the capOS gateway. Anything that can issue HTTP requests to the loopback listener inherits that authority. Today the bridge has:

  • A single shared Arc<Mutex<AppState>> constructed once in run() (around line 1606) and cloned to every accepted connection.
  • No per-browser session cookie, no per-tab token, no per-origin isolation, no proof-of-possession of the original operator login.
  • An origin allow-list that returns true when the Origin header is absent (origin_allowed, line 2163-2169), which lets non-browser POSTs bypass the only state-change guard.
  • A non-constant-time != comparison on the automation token (automation_report at line 1184 and set_automation_report at line 1198).
  • Plain http://127.0.0.1:<port>/ transport.

Treating 127.0.0.1 as a trust boundary repeats the failure pattern of historic Docker, Jupyter, and Electron loopback CVEs: any local user, another OS account, a malicious browser extension, a locally-running package install script, or any other process that can connect(2) to the listener can drive the upstream capOS gateway with the operator’s authority. Two browsers today silently share one upstream session; there is no way for an operator or audit log to distinguish them.

Required Posture

Per-browser BrowserSession

Mint a high-entropy opaque session id at the first browser hit and store it server-side as a BrowserSession record distinct from the upstream capOS session. The cookie is the only thing the browser holds; everything else stays in AppState. Two browsers must end up with two BrowserSession records.

Cookie attribute target:

  • HttpOnly
  • SameSite=Strict
  • Path=/
  • Host-only: no Domain attribute.
  • __Host- name prefix when transport allows it (requires Secure and Path=/ and forbids Domain).
  • Max-Age=... plus an absolute upper bound enforced server-side.

Secure cookie attribute over plaintext loopback is browser- and version-specific. Modern browsers do treat 127.0.0.1 and ::1 as potentially trustworthy origins for some Secure-Context APIs, but acceptance and sending of Secure-flagged cookies over plaintext loopback is not uniform across vendors and versions. Two acceptable deployment paths:

  1. Move the bridge to HTTPS or to the Tauri custom-scheme secure origin before requiring Secure and __Host-.
  2. Run on plaintext loopback as an interim with HttpOnly; SameSite=Strict; Path=/; Max-Age=... and no Secure / __Host-, with a documented support matrix and a test that proves browsers retain and resend the cookie across the supported range.

The implementation must pick one and document the choice here. The design must not silently rely on a Secure flag that some target browsers drop.

Server-side requirements:

  • High-entropy opaque ids; never derived from user-controlled input.
  • Server-side rotation: regenerate the BrowserSession id on successful login and on privilege transitions, and invalidate the prior anonymous/pre-auth record. (Session-fixation defense.)
  • Server-side invalidation on logout, idle timeout, absolute timeout, and explicit revoke; wipe the record from AppState.
  • Cookie value must never be logged, never written to the transcript, and never included in any DTO returned to the browser.

Multi-browser policy

Pick one and document it here:

  • (a) Independent logins. Each BrowserSession carries its own upstream capOS session; logging in from a second browser opens a second upstream session.
  • (b) First-wins exclusivity. The first authenticated BrowserSession owns the upstream session; subsequent browsers see an explicit “session already in use” denial DTO rather than silent piggy-backing.

Either is acceptable if explicit and audit-logged. Silent shared state is not.

CSRF and origin discipline

  • Require a valid BrowserSession cookie on every /api/* route, not only state-changing routes. Today’s GET /api/state, GET /api/transcript/redacted, and GET /api/automation/report expose state, transcript, and automation surfaces and must not rely on SOP/loopback assumptions alone.
  • Reject state-changing requests when Origin is missing. The current origin_allowed short-circuit on missing Origin (line 2164) must be removed for state-changing methods. Validate Origin against the listener’s expected loopback origin set, and validate Referer as a fallback only when Origin is absent on legacy paths.
  • Add a double-submit CSRF token bound to the BrowserSession cookie and required on every state-changing POST. SameSite=Strict is not sufficient defense in depth on its own.
  • Defense-in-depth via Fetch Metadata: reject browser POSTs whose Sec-Fetch-Site is cross-site or whose Sec-Fetch-Mode is not in the expected set for the route. This is not a replacement for CSRF/Origin, but adds another layer.

DNS-rebinding hardening

Validate the Host header against the loopback set {127.0.0.1:<port>, localhost:<port>, [::1]:<port>}. Without this, DNS-rebinding from a malicious public site can use the victim’s browser as a proxy into the loopback bridge.

Content-Type enforcement

Reject POSTs whose Content-Type is not application/json (or the specific expected type for the route). This blocks text/plain / form-urlencoded cross-origin form submits that bypass preflight.

CORS stance

Emit no Access-Control-Allow-Origin by default. If a future route ever needs CORS, allow only the exact same-origin echo of the listener URL. Refuse wildcards. Refuse Access-Control-Allow-Credentials: true combined with permissive origins. Document the rule in code so future contributors do not accidentally widen it.

Security response headers

Send on every response (HTML and API):

  • X-Frame-Options: DENY (anti-clickjacking).
  • X-Content-Type-Options: nosniff.
  • Referrer-Policy: no-referrer.
  • Cross-Origin-Opener-Policy: same-origin.
  • Cross-Origin-Embedder-Policy: require-corp.
  • Cross-Origin-Resource-Policy: same-origin for API responses.
  • Cache-Control: no-store for API responses.

Content-Security-Policy target: no unsafe-inline for script-src or style-src. The current UI conflicts with that target: tools/remote-session-client/ui/index.html has an inline feature-flag script (around lines 11-35) and inline style="..." attributes (e.g. lines 91, 154, 179, and others). Roll out the CSP as part of a single change set that:

  1. Moves the inline feature-flag script into a static asset (e.g. assets/feature-flags.js) loaded from a fixed allow-list path.
  2. Lifts inline style attributes into the existing stylesheet, or
  3. For any block intentionally retained inline, uses per-response CSP nonces or precomputed hashes.

A literal CSP rollout without that refactor breaks the UI.

Constant-time secret comparison

Replace the automation.token != token checks (lines 1184 and 1198) with a constant-time comparator. Apply the same comparator to the future BrowserSession cookie value lookup, the CSRF token check, and any future bearer/HMAC validations.

Auth-endpoint rate limiting and lockout

Add per-BrowserSession and per-listener rate limits to /api/login/password and any future credential-handling routes. Exponential backoff on failure. Audit-logged lockout. Wire into the same audit stream as upstream session events so the operator sees failed attempts.

Idle and absolute timeouts

Independent of the upstream capOS session expiry, expire BrowserSession cookies on idle and on absolute lifetime. Force re-auth on resume. Rotate the cookie id on re-auth.

Log injection / transcript safety

Sanitize browser-supplied strings routed into the transcript or stderr for CRLF, ANSI escape sequences, and control bytes so a hostile client cannot forge transcript rows or terminal control on operator stderr.

DTO-only-to-webview discipline

Keep the existing *Vm DTO boundary in tools/remote-session-client/src/bin/remote_session_ui.rs (lines ~199-382). The browser must never receive raw cap handles, raw interface ids, or unredacted session ids. The CapVm.interface_id field is already #[serde(skip_serializing)]; preserve that pattern for any new fields.

Tauri Wrapper

The planned Tauri wrapper is a future component; the repository today contains only tools/remote-session-tauri-preflight.sh and no src-tauri/, no Tauri config, no invoke commands. The threat model for the wrapper differs from the loopback HTTP bridge:

  • No loopback HTTP listener. The wrapper should not expose a TCP port between webview and Rust. Use Tauri command IPC (core.invoke, or window.__TAURI__ when globally exposed) and packaged/custom-protocol assets. The exact transport between webview and Rust core is platform-dependent (WebKit2GTK on Linux, WKWebView on macOS, WebView2 on Windows) and is not appropriate to pin in this proposal.
  • Origin / Host / DNS-rebinding controls do not translate. With no TCP port and no HTTP transport between webview and Rust, those controls do not apply directly. Do not copy them blindly.
  • Webview content is the attacker. If any non-trusted asset can ever load (remote frame, broken integrity check, mis-scoped asset protocol), webview JavaScript becomes the attacker. CSP, asset scope discipline, no remote frames, no eval-style hatches still apply.
  • Capability/allowlist minimization. Lock the Tauri capability manifest tightly. Every invoke command and every core API (fs, shell, http, dialog, process, window, clipboard, …) the frontend may call must be enumerated and minimized. Misconfigured Tauri allowlists are the dominant Tauri CVE pattern; prefer per-window capability scoping over global allow.
  • Per-window BrowserSession isolation. If multiple windows are spawned over a shared Rust state, keep per-window BrowserSession isolation matching the loopback design.
  • Carry-over controls. Constant-time secret comparison, rate-limiting, idle/absolute timeouts, transcript-injection sanitization, DTO-only-to-webview discipline, and audit logging apply to the Tauri wrapper unchanged.

Verification

Before this finding is closed in REVIEW_FINDINGS.md:

  • Host tests cover each control above (cookie attributes, CSRF guard, Origin/Host validation, Content-Type rejection, CSP surface, header set, constant-time compare, rate limit, timeouts, log injection sanitization).
  • The CSP refactor of tools/remote-session-client/ui/index.html ships in the same change set as the CSP header.
  • The cookie-transport choice (HTTPS/secure-origin vs. interim plaintext-loopback no-Secure) is recorded in this proposal and the matching browser support matrix is documented.
  • The multi-browser policy choice is recorded in this proposal and reflected in audit logs and DTO denial diagnostics.
  • The Tauri wrapper, when introduced, ships with a minimized capability manifest and the carry-over controls verified.