# 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.
