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 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. - Plain
http://127.0.0.1:<port>/transport.
Already closed:
- The previous non-constant-time
!=comparison on the automation token has been replaced withconstant_time_eqinautomation_reportandset_automation_report(seetools/remote-session-client/src/bin/remote_session_ui.rs:1378and:1392). Future secret comparisons must use the same comparator. - The loopback bridge now mints per-browser
BrowserSessioncookies, requires CSRF tokens on state-changing/api/*routes, validatesHost/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:
HttpOnlySameSite=Strict(the loopback bridge has no cross-site sign-in redirect, soStrictis unconditional here; the capOS-servedremote-session-web-uibehind public ingress selects the posture from the boot manifest instead –Strictby default,Laxonly when an IAP-fronted deployment manifest grants theiap_fronted_ingressmarker, per the selected policy incloud-deployment-proposal.md– and applies it uniformly to the session, CSRF, and clear-cookie headers)Path=/- 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.
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):
| Browser | Min version | Notes |
|---|---|---|
| Chromium | 96+ | 127.0.0.1 is a potentially-trustworthy origin |
| Firefox | 96+ | same; SameSite=Strict enforced for loopback |
| Safari | 15.4+ | macOS 12.3+ / iOS 15.4+ |
| Edge | 96+ | 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
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.
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/guestafter 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 inAppState. - 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 typedsessionAlreadyInUsedenial 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
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.
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 class | Trusted owner or denial boundary | Proof / denial expectation |
|---|---|---|
| Raw capOS capabilities, raw cap handles, raw interface ids, and local cap ids | Held 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 creation | Owned 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 authority | Kept 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 TcpListenAuthority | remote-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 factories | The 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 rules | Public-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 slots | Stored 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 hints | Limited 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-
BrowserSessioncookies, 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
invokecommands 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
packagemode 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
invokecommand 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
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.
- Desktop automation remains unreviewed. The wrapper’s
automationmode 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.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 check/dev scaffold keeps the existing loopback
bridge controls in force, has no app-specific remote-session
invokecommands, 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.