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 Tauri desktop wrapper inherits the controls. It also records which browser-facing controls carry over to the capOS-served remote-session-web-ui service and which public-origin controls belong to the selected GCE provider-terminated HTTPS policy, without authorizing public exposure. It is cross-linked from docs/proposals/security-and-verification-proposal.md, docs/proposals/remote-session-capset-client-proposal.md (the parent proposal that defines the remote session CapSet wire and host-client shape this bridge instantiates), and the design risks register entry R17 – Remote-session UI bridge and Tauri wrapper are research-only, which routes long-horizon residual risk (distributable packaging, desktop automation, non-loopback exposure) back to this proposal.

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. The original bridge shape had:

  • 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.
  • Plain http://127.0.0.1:<port>/ transport.

Already closed:

  • The previous non-constant-time != comparison on the automation token has been replaced with constant_time_eq in automation_report and set_automation_report (see tools/remote-session-client/src/bin/remote_session_ui.rs:1378 and :1392). Future secret comparisons must use the same comparator.
  • The loopback bridge now mints per-browser BrowserSession cookies, requires CSRF tokens on state-changing /api/* routes, validates Host / Origin / JSON content type before route work, and enforces first-wins bridge ownership through an atomic tentative reservation.
  • The local HTTP parser now bounds request-line length, header-line length, header count, aggregate header bytes, body size, slow reads, and concurrent handler threads before gateway or authentication work.

Gateway-host redirect scope. POST /api/config is intentionally operator-controlled: it allows an authenticated operator to point the bridge at a different gateway_host. This is bounded by the operator-console trust boundary — only a caller who has already passed the BrowserSession cookie guard and the CSRF double-submit check (i.e., the bridge-owning operator session) can invoke it. The capability model provides the deeper guarantee: the bridge holds a single capOS gateway connection at a time; redirecting to an arbitrary host replaces that connection but does not grant new capability authority that wasn’t already present in an authenticated operator session. No arbitrary-host proxy to untrusted endpoints is possible without an authenticated operator action.

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 (the loopback bridge has no cross-site sign-in redirect, so Strict is unconditional here; the capOS-served remote-session-web-ui behind public ingress selects the posture from the boot manifest instead – Strict by default, Lax only when an IAP-fronted deployment manifest grants the iap_fronted_ingress marker, per the selected policy in cloud-deployment-proposal.md – and applies it uniformly to the session, CSRF, and clear-cookie headers)
  • 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.

Decision: option 2 (plaintext loopback, no Secure, no __Host-). This matches the current research-stage operator-bridge deployment, which only listens on 127.0.0.1 and is not reachable from the network. Cookie attributes are therefore HttpOnly; SameSite=Strict; Path=/; Max-Age=<absolute-timeout-secs> exactly – no Secure and no __Host- prefix. The follow-on Tauri / HTTPS track will switch to option 1 (with Secure and __Host-) before shipping beyond research use; the cookie-emit code carves out one place to flip both attributes when transport changes.

Browser support matrix verified for option 2 (cookies retained and resent across loopback HTTP without Secure):

BrowserMin versionNotes
Chromium96+127.0.0.1 is a potentially-trustworthy origin
Firefox96+same; SameSite=Strict enforced for loopback
Safari15.4+macOS 12.3+ / iOS 15.4+
Edge96+matches Chromium

The verification host test in iter7 round-trips a cookie through a synthetic loopback request to assert browsers within this matrix retain and resend it. Older browsers (pre-Same-Site-Strict enforcement on loopback) are not supported.

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.

Decision: option (b), first-wins exclusivity. The bridge today holds exactly one upstream capOS session per process, and the research-stage operator boot does not have a clean way to multiplex two operator-authority sessions through a single capOS gateway connection. First-wins is also the auditor-friendlier path: every denied “session already in use” carries the active BrowserSession’s timestamped lineage, so the operator and audit log see the rejection rather than silently sharing state. Concretely:

  • The first browser that starts /api/login/password, /api/login/anonymous, or /api/login/guest after passing local request guards reserves the owner slot before upstream gateway authentication. Successful login rotates the BrowserSession id, marks that slot authenticated, and keeps the upstream capOS session handle in AppState.
  • Failed local login validation, bad credentials, and gateway denials release the tentative reservation. An already authenticated owner is not released by a later bad retry from the same browser session.
  • Subsequent BrowserSessions authenticating against the same bridge get a typed sessionAlreadyInUse denial DTO rather than an upstream login attempt, including while the first session is still authenticating upstream. The denial includes the owner’s claim or authentication timestamp so the second operator sees when the bridge was claimed.
  • Logout / idle-timeout / absolute-timeout on the owner releases the upstream session and clears owner_session_id; the next authenticator wins.
  • Every transition (claim / denial / release) emits a structured audit event into the same stream as upstream capOS session events so an operator looking back can see the bridge contention pattern.

The Tauri wrapper inherits this rule per-window unless the wrapper introduces an explicit multi-window upstream-fanout authority the loopback bridge does not have.

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.

Implemented on both surfaces. The capOS-served remote-session-web-ui normalizes the header (casing and ;-parameters stripped) and requires the application/json media type on every state-changing /api/* POST class – login-family and authenticated – before route work, with a typed 415 denial (missingContentType / unsupportedContentType). The more specific Host/Origin denials keep precedence, and the fixed non-JSON routes (/healthz, bundle assets, the scoped ACME http-01 challenge path) are unaffected. make run-cloud-prod-remote-session-web-ui-l4 proves the negative matrix (missing, text/plain, form-encoded, multipart, malformed, mixed-case parameterized non-JSON) and the parameterized/mixed-case JSON positives over the real ingress path. This is local request-shape hardening only; it is not public ingress or TLS readiness.

Local HTTP request and handler bounds

The host bridge remains a trusted local development bridge. These bounds reduce local resource-exhaustion and confused-client failure modes; they do not make the UI a public network service.

The HTTP parser must reject overlong request lines, overlong header lines, too many headers, excessive aggregate header bytes, and overlarge bodies before route dispatch, JSON parsing, authentication, or gateway I/O. Incomplete or slow request lines, headers, and bodies must time out under a fixed read deadline. The accept loop must also cap concurrent request handler threads and fail closed with a typed local denial rather than spawning one thread per accepted connection without bound.

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

Implemented in the in-guest remote-session-web-ui service (SECURITY_RESPONSE_HEADERS / CONTENT_SECURITY_POLICY in demos/remote-session-web-ui/src/main.rs), emitted on every response class – HTML, static assets, JSON API, /healthz, the ACME http-01 challenge route, and every denial:

  • 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.
  • Cache-Control: no-store.

The implemented shape applies Cross-Origin-Resource-Policy: same-origin and Cache-Control: no-store to every response, not only API responses: every asset is consumed same-origin by the operator app, non-browser consumers (provider health checkers, ACME validators) ignore browser embedding policy, and serving the fixed boot-resource bundle uncached is acceptable for the operator UI. Relaxing caching for static assets would be a deliberate future change, not a default.

The implemented Content-Security-Policy meets the no-unsafe-inline target for both script-src and style-src:

default-src 'none'; script-src 'self'; style-src 'self';
img-src 'self' data:; connect-src 'self'; base-uri 'none';
form-action 'self'; frame-ancestors 'none'

img-src allows data: in addition to 'self' because the committed stylesheet’s hacker-theme dashed border is a data:image/svg+xml background image; a data: image cannot execute script under this policy, and folding it into the pinned bundle as a file asset would be a separate reviewed bundle change. The earlier inline feature-flag script and inline style="..." attributes in tools/remote-session-client/ui/index.html were moved into static bundle assets (/feature-flags.js, the stylesheet) before the CSP landed, so the strict policy serves the fixed bundle without nonces or hashes. The local QEMU proof (make run-cloud-prod-remote-session-web-ui-l4) asserts the header set and CSP on every response class over the real ingress, boots the served root document in a real browser under the strict CSP with zero securitypolicyviolation events, and asserts no Access-Control-* header is emitted on any probed route.

Constant-time secret comparison

The automation-token check has been migrated to constant_time_eq (automation_report and set_automation_report in tools/remote-session-client/src/bin/remote_session_ui.rs). 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.

Self-Served And Public-Origin Carry-Over

The host-local remote-session-ui bridge and the capOS-served remote-session-web-ui service are different deployment surfaces. The host bridge is a trusted Linux loopback development tool whose backend owns the TCP gateway connection. The self-served service is a capOS userspace HTTP service that owns its TcpListenAuthority, session-manager login flow, authority-broker bundle, and remote CapSet/proxy state inside the guest. The host bridge is not the self-served service moved into the guest.

The authority boundary is the shared rule. Browser JavaScript receives only view models, typed commands, typed results, denials, redacted transcript/status rows, and fixed UI assets. It must not receive raw capOS capabilities, raw cap ids, endpoint-owner authority, ProcessSpawner, socket factories, NetworkManager, TcpListenAuthority, TcpListener, TcpSocket, key material, remote CapSet handles, result-cap slots, process handles, host usernames, host paths, host environment markers, or QEMU-forwarding identity hints. These exclusions match the self-served Gate 1B boundary in Remote Session CapSet Client and the implementation proof records under remote-session-self-served-full-ui-bundle.

Forbidden browser-visible surface matrix:

Forbidden browser-visible classTrusted owner or denial boundaryProof / denial expectation
Raw capOS capabilities, raw cap handles, raw interface ids, and local cap idsHeld only by the remote-session-web-ui backend, its server-side proxy state, or the upstream gateway connection.Browser envelopes, DOM state, diagnostics, transcripts, and JSON contain only DTO names and redacted labels; any browser request that tries to name a cap id fails before backend dispatch.
Endpoint-owner authority and arbitrary endpoint creationOwned by the backend service runner and AuthorityBroker policy, not by browser state.Browser launch forms name only approved service descriptors; denied launches return typed denial DTOs without endpoint-owner tokens or creation handles.
Process handles, raw ProcessSpawner, and shell launcher authorityKept behind AuthorityBroker-approved remote-client bundle policy.Status and transcript rows expose only redacted process/service state; process handles and spawner markers are absent from browser-visible data.
NetworkManager and TcpListenAuthorityremote-session-web-ui owns only the manifest-scoped UI listener for the selected proof target; the open cloudboot L4 task must source that listener through the Phase C userspace network path rather than browser or raw manager authority.Listener/source metadata is service-derived from the accepted socket plus a service event id; browser requests cannot supply trusted source, route, or listener authority.
TcpListener, TcpSocket, and socket factoriesThe HTTP accept loop owns accepted sockets and per-connection state server-side.Browser JavaScript uses ordinary same-origin HTTP commands only; socket factory names, accepted-socket handles, and backend connection handles never appear in DTOs.
Key material, TLS private keys, certificates, public IPs, and firewall rulesPublic-origin TLS and ingress remain in the on-hold provider-terminated HTTPS task; local and private proofs do not hold these secrets in the browser or capOS Web UI.Local self-served and cloudboot proofs must not emit TLS key/certificate material, provider resource ids, public addresses, or firewall rule names as browser-readable state.
Remote CapSet handles, backend cap holders, session-global ids, and result-cap slotsStored in server-side remote-session proxy tables and invalidated through backend logout/stale-call rules.Browser commands reference typed route/request ids only; stale calls and unauthorized result access fail closed without leaking slot numbers or remote handles.
Host paths, host usernames, host environment markers, and QEMU-forwarding identity hintsLimited to development harness/operator context and not part of the capOS-served browser contract.DOM state, JSON responses, diagnostics, and transcripts use redacted service labels; source metadata is backend-derived and cannot be replayed from browser-supplied fields.

The matrix is a review checklist, not the enforcement mechanism. The browser boundary is acceptable only when the backend also rejects stale, unauthorized, or client-supplied authority selectors before any capability dispatch.

The carry-over controls are backend-held session state, server-side BrowserSession records, CSRF tokens on state-changing JSON routes, Host/Origin/Referer/content-type validation, no wildcard CORS, security response headers, request and handler bounds, per-session rate and resource limits, idle and absolute lifetime enforcement, logout that drops server-side authority, transcript sanitization, constant-time comparisons for secrets, and audit-visible denials. Those controls are required for the capOS-served service as well as the loopback bridge, but their concrete transport assumptions differ.

On the capOS-served remote-session-web-ui, the browser-boundary baseline is implemented and locally proven on make run-cloud-prod-remote-session-web-ui-l4: server-side session hardening (unpredictable rotated session ids, a domain-separated double-submit CSRF token, Host/Origin validation, and idle/absolute lifetime enforcement), GFE-range-pinned forwarded-scheme trust, the manifest-selected single public origin, the IAP-aware SameSite cookie posture, JSON content-type rejection on state-changing /api/* POSTs, the uniform security response headers with the strict no-unsafe-inline CSP, in-guest login peer-gating with failure backoff, and the public /healthz health-check contract. All of that evidence is local QEMU/cloudboot proof only; none of it claims private GCE reachability, public ingress, TLS custody, or operator exposure.

Two browser-boundary local proofs remain open as dispatchable task records under docs/tasks/, not done: a public-deployment loopback gate that rejects loopback Host/Origin/Referer acceptance and loopback-shaped source hints when the public-origin load-balancer posture is configured (the landed local proofs intentionally preserve the QEMU loopback posture), and a consolidated browser-visible forbidden-marker matrix proof that scans every response class – success, denial, health, manual, and error bodies – for the forbidden surface above and proves hostile browser-supplied authority fields fail closed before backend-held capability dispatch.

Loopback-only decisions do not carry to a public origin. The plaintext http://127.0.0.1 cookie exception above is only for the trusted local bridge. A public operator endpoint must use the selected policy in Cloud Deployment: one HTTPS origin at a GCP external Application Load Balancer, no wildcard CORS or cross-origin credentialed requests, provider-terminated TLS with no capOS or harness private-key custody for the bootstrap proof, capOS serving only plain HTTP/1.1 on the backend port, no public IP on the VM, and firewall-bounded trust in the load balancer’s forwarded-scheme headers. Public sessions use Secure/HttpOnly/SameSite cookies, HSTS at the HTTPS edge, CSRF Origin/Referer checks against the known public origin, bounded idle and absolute lifetimes, and server-side logout.

The forwarded-scheme half of that trust boundary is already implemented and locally proven on the capOS-served service: remote-session-web-ui honors X-Forwarded-Proto only from the recorded GCP front-end source ranges (130.211.0.0/22, 35.191.0.0/16) and treats the header from any other peer – or any unknown peer-address format – as absent, so a direct client cannot forge secure-context cookie posture. make run-cloud-prod-remote-session-web-ui-l4 drives both the forged-header negative over the real ingress path and the trusted-forwarder fixture positive.

The single-public-origin half is also implemented and locally proven: remote-session-web-ui reads exactly one public_origin.<host> manifest marker cap (fail-closed on a second marker, a malformed, loopback-named, or IP-literal-shaped host, or any unrecognized extra grant) and accepts the configured https://<host> origin in its Host/Origin/Referer gates only for requests arriving through the trusted forwarded-scheme HTTPS path. Cross-origin, mixed-scheme, wildcard, and missing-origin state changes fail closed before backend-held capability dispatch, browser-supplied principal/source hint headers are rejected on the public-origin path, no CORS headers are ever emitted, and the loopback proof posture is unchanged. The same proof drives a direct-client forged public Host/Origin negative over the real ingress and the trusted-forwarder fixture positive in-process. This is local public-origin readiness only – no DNS name, load balancer, TLS endpoint, or live public exposure is claimed.

Keep the proof classes separate. The landed local/QEMU self-served UI bundle proof does not prove local cloudboot L4 over the Phase C userspace network stack. The local cloudboot L4 proof does not prove private GCE reachability. The private GCE proof does not authorize public IPs, firewall exposure, DNS, TLS certificates, or operator browser exposure from the internet. The later cloud-gce-public-self-hosted-webui-ingress-tls task remains on hold for explicit public-ingress/TLS authorization and must build against the selected provider-terminated HTTPS policy rather than raw public HTTP.

Tauri Wrapper

The repository now contains a check/dev Tauri wrapper scaffold under tools/remote-session-client/src-tauri/. It does not introduce a new remote-session authority boundary: make remote-session-tauri checks the wrapper and host Tauri prerequisites by default, and CAPOS_REMOTE_SESSION_TAURI_MODE=dev make remote-session-tauri launches cargo tauri dev. The webview loads http://127.0.0.1:3337/ from the existing remote-session-ui Rust backend, so the backend still owns the gateway TCP connection, remote session state, remote caps, and worker proxies. Webview JavaScript receives only the same view models, user events, typed results, denials, and redacted transcript rows as the trusted local web bridge.

The wrapper command also has a policy-only preflight: CAPOS_REMOTE_SESSION_TAURI_MODE=policy tools/remote-session-tauri.sh. That preflight runs before Tauri dependency/build checks in the normal check path and does not require Tauri Linux packages or a desktop session. It fails closed if the reviewed scaffold drifts: bundling must stay disabled, both the Tauri devUrl and the single main window URL must remain http://127.0.0.1:3337, the default capability must grant only core:default to the main window, and the wrapper must not add app-specific Tauri commands, invoke handlers, generate handlers, or tauri-plugin-* dependencies/uses. This is a guardrail over the current check/dev scaffold only; it is not evidence that distributable packaging or desktop automation is reviewed.

The current check/dev wrapper therefore inherits the loopback HTTP bridge threat model:

  • Loopback HTTP controls apply. Host validation, Origin checks, CSRF tokens, per-BrowserSession cookies, request bounds, first-wins ownership, rate limiting, transcript sanitization, and DTO-only-to-webview discipline apply to the Tauri webview path unchanged because the webview talks to the same loopback backend.
  • No custom Tauri invoke authority. The current scaffold has no app-specific invoke commands for remote-session actions. Do not add Tauri commands that expose raw caps, cap ids, process handles, endpoint owner caps, result slots, host usernames, host paths, or gateway connection internals to the webview.
  • Distributable packaging is still residual. Bundling is disabled until the backend lifecycle is reviewed. A future packaged wrapper may keep a reviewed loopback sidecar or migrate to Tauri command IPC / custom-protocol assets, but that change must update this proposal and re-evaluate which loopback controls still apply. The wrapper’s package mode is intentionally blocked until that review is done.
  • 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 before distributable packaging is enabled. 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.
  • Desktop automation remains unreviewed. The wrapper’s automation mode is intentionally blocked until screenshot/input authority, automation-token handling, UI-smoke oracle scope, desktop session isolation, and fail-closed teardown have a reviewed design.

Verification

Before the corresponding review-finding task is closed:

  • 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 check/dev scaffold keeps the existing loopback bridge controls in force, has no app-specific remote-session invoke commands, leaves distributable packaging disabled until the sidecar/custom-protocol/backend lifecycle is reviewed, and keeps the policy preflight passing as a narrow guardrail over that scaffold.