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 inrun()(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
truewhen theOriginheader 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_reportat line 1184 andset_automation_reportat 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:
HttpOnlySameSite=StrictPath=/- Host-only: no
Domainattribute. __Host-name prefix when transport allows it (requiresSecureandPath=/and forbidsDomain).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:
- Move the bridge to HTTPS or to the Tauri custom-scheme secure
origin before requiring
Secureand__Host-. - Run on plaintext loopback as an interim with
HttpOnly; SameSite=Strict; Path=/; Max-Age=...and noSecure/__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
BrowserSessionid 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
BrowserSessioncarries its own upstream capOS session; logging in from a second browser opens a second upstream session. - (b) First-wins exclusivity. The first authenticated
BrowserSessionowns 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
BrowserSessioncookie on every/api/*route, not only state-changing routes. Today’sGET /api/state,GET /api/transcript/redacted, andGET /api/automation/reportexpose state, transcript, and automation surfaces and must not rely on SOP/loopback assumptions alone. - Reject state-changing requests when
Originis missing. The currentorigin_allowedshort-circuit on missingOrigin(line 2164) must be removed for state-changing methods. ValidateOriginagainst the listener’s expected loopback origin set, and validateRefereras a fallback only whenOriginis absent on legacy paths. - Add a double-submit CSRF token bound to the
BrowserSessioncookie and required on every state-changing POST.SameSite=Strictis not sufficient defense in depth on its own. - Defense-in-depth via Fetch Metadata: reject browser POSTs whose
Sec-Fetch-Siteiscross-siteor whoseSec-Fetch-Modeis 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-originfor API responses.Cache-Control: no-storefor 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:
- Moves the inline feature-flag script into a static asset (e.g.
assets/feature-flags.js) loaded from a fixed allow-list path. - Lifts inline style attributes into the existing stylesheet, or
- 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, orwindow.__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
invokecommand 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
BrowserSessionisolation 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.htmlships 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.