Skip to main content
Version: 0.1.0-alpha.5 (alpha)

Browser and hydration boundary: same-origin-v1

Issue #235 adds a deliberately narrow browser policy to the existing shared standalone HTTP owner. It does not add an application listener, renderer pool, cookie authenticator, HTML sanitizer, or stronger guest execution profile. Linux x86-64 remains the qualified standalone serving profile. The HTTP WIT contract and the Angular T0/T1 admission gate are unchanged.

Origin, tenant and principal​

The transport fixes the scheme: http for the loopback development profile, https for direct TLS or the explicitly allowlisted TLS-terminating proxy profile. The canonical Host authority and host-authenticated principal select the tenant. Forwarded headers cannot supply scheme, host, base URL or identity. Outside the proxy profile, Forwarded and X-Forwarded-* are rejected. Inside it they are discarded, not trusted. Authorization, proxy authorization, trace context, X-Real-IP, remote-user, original/rewrite-URL, X-Auth-Request-* and X-Authenticated-* fields are not application identity channels. The existing X-Lsf-* prohibition remains. Other application-visible headers are untrusted data; only the host context is identity authority.

public-origins continues to map anonymous requests to its configured low- privilege tenant principal, not to an authenticated end user. Its existing authority/tenant bindings are also its browser bindings; a separate nonempty browserOrigins setting is rejected. Bearer ingress is native-only by default: requests containing Origin or Fetch Metadata require an explicit binding inside httpIngress:

{
"browserOrigins": [{"authority": "web.example.test", "tenant": "example"}]
}

This optional closed array contains at most 32 distinct canonical authorities, each associated with an existing invoke-credential tenant. It does not contain a scheme or a credential. Once configured, every request must match its Host and authenticated tenant, including native calls and immutable asset requests. Administrator/RPC credentials are not browser authority. Browser metadata is not authentication; a native client can fabricate it and must still authenticate.

An origin is a browser security boundary, not a URL path. Do not place mutually untrusted tenants, applications, uploads or executable assets on one origin. Different paths and immutable publication digests do not isolate their DOM, cookies, same-origin scripts or browser storage. Avoid sharing an origin between untrusted applications even when they share an LSF tenant.

Requests, CORS and CSRF​

An opt-in asset extension is specified in ADR-0049. Its browser experiment establishes feasibility only; native CORS support remains pending. The current rules below continue to apply to running nodes.

  • An Origin must equal the exact serialized configured origin. null, lists, alternative/default-port spellings, trailing slashes and foreign origins are rejected. Duplicate Origin or supported Fetch Metadata fields are rejected.
  • By default, Sec-Fetch-Site permits only same-origin and none. Supported modes are navigate, same-origin, cors and no-cors; supported destinations are document, empty, script, style, image, font and manifest. Sec-Fetch-User, when present, must be ?1.
  • CORS is not supported. Preflight request fields are rejected, no access-control response headers are emitted, and guests cannot opt back in. This also means external-site links/subresources with cross-site Fetch Metadata are rejected unless a static document navigation is explicitly enabled below. A safe address-bar/new-tab navigation with none is supported.
  • Public or browser unsafe methods require a matching Origin. none is not accepted for unsafe methods. GET, HEAD and OPTIONS must remain free of state changes in application code. Cookie values never authenticate a platform call. Native bearer requests without browser metadata retain their existing API use.

This is an origin-based CSRF profile, not a synchronizer-token or login/session framework. Legacy clients lacking an Origin on unsafe public calls fail closed. The host emits Referrer-Policy: same-origin: cross-origin referrers are withheld without making same-origin non-CORS POST Origin become null, as the Fetch Origin-header algorithm would do for no-referrer. Browser tests exercise an actual same-origin POST.

Host-owned response policy​

To let readers follow links from other websites, add a publicDocumentNavigation array inside httpIngress. Each entry supplies an existing public authority, its tenant, and a mount, such as /docs or /. The authority and tenant must match authentication.origins under the public-origins adapter. A maximum of 32 distinct authority/mount pairs is allowed. Omission or an empty array keeps the strict policy.

Only top-level GET/HEAD document navigations to admitted static HTML qualify. Both same-site and cross-site require navigate mode, document destination and no Origin header. Duplicate or malformed fields are rejected; incomplete metadata does not qualify for the exception. Directory redirects are checked again at their destination. /docs includes /docs/guide, not /docs-other.

This lets the reader open a public page. It does not let the linking website fetch its scripts, read its API, embed it in a frame, or submit cross-origin forms. Application capsule routes and immutable asset URLs remain outside the exception. A native caller can forge browser metadata but cannot bypass tenant, route, media or current publication eligibility checks. See ADR-0046.

Response headers​

Static HTML can additionally use the bounded, signed style identities described in ADR-0052 when the host explicitly enables httpIngress.allowStaticStyleHashes. The host adds only those SHA-256 sources to style-src. Script policy, style-attribute restrictions and every other directive remain unchanged. The same selected publication policy appears on HTML HEAD, 304 and directory redirects; errors keep the strict policy. No nonce or HTML rewrite is involved. See the framework guide for application changes.

All writable dynamic, error and immutable-asset responses, including HEAD and 304, receive these headers. A failed/expired transport may close without a response; it does not promise to write headers after losing ownership.

Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self'; font-src 'self'; connect-src 'self'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'; object-src 'none'; worker-src 'none'; manifest-src 'self'
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Resource-Policy: same-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=()

HTTPS responses additionally carry Strict-Transport-Security: max-age=31536000, without includeSubDomains or preload. Deploy HTTPS before using browser cookies. No executable inline script, inline style, eval, blob/data script, remote script, worker, iframe embedding or base-element override is allowed by this profile. Use AOT Angular, external same-origin bootstrap/CSS and URLs derived from the admitted publication. The controlled fixture uses no inline component styles. This work does not rewrite Angular output to fit that policy.

Guests cannot override these headers, set any Access-Control-*, or emit Refresh, Content-Location, Link, Clear-Site-Data, Report-To, NEL, CSP-report-only or COEP. Invalid guest output becomes a fixed non-reflective, no-store 502 before any bytes or pending cache fill are committed. This preserves the delivery lease and existing cancellation/cleanup accounting.

Redirects 301/302/303/307/308 require exactly one canonical root-relative Location; 201 may have one. Other statuses may not have one. Absolute URLs, protocol-relative URLs, fragments, dot/encoded-separator aliases, backslashes and CR/LF fail closed. The browser's actual origin supplies the base, never guest or proxy metadata.

Dynamic HTML must declare exactly text/html; charset=utf-8 (case-insensitive) and contain valid UTF-8. Other declared media remain subject to the existing bounded MIME grammar. A missing dynamic media type becomes application/octet-stream. Immutable assets retain their admitted manifest MIME and exact bytes; HTML authors must declare UTF-8 in their document, as the fixture does. Assets are not rewritten, extension-sniffed by the HTTP owner or served from unlisted paths.

Cookies, framing and encoded bodies​

The request cookie profile is at most 16 distinct names and 4,096 aggregate header-field-value bytes across Cookie fields. Names are 1-64 HTTP token bytes; values are at most 1,024 RFC cookie-octets, without quoting, whitespace or control characters. Duplicate names and malformed pairs are rejected, not resolved by precedence. Count/aggregate overflow returns 431; malformed pairs return 400.

Response cookies are HTTPS-only, unique __Host- names with nonempty suffixes. Every cookie requires the exact attributes Secure; HttpOnly; SameSite=Strict; Path=/, in any order and without duplicates. Only Max-Age=0 deletion is an optional additional attribute. Domain, Expires, persistent positive lifetimes, other SameSite modes and script-readable cookies are unsupported. The same name/value/count/aggregate limits apply. Cookies still belong to application session logic and never change the platform principal.

The existing 64-field/16-KiB header budget, 32-KiB raw head, strict CRLF framing, 64-KiB request body and 256-KiB dynamic response body remain. Host security headers have fixed additional output overhead; the asset head allowance is 2 KiB inside the existing exchange/connection reservation. Duplicate framing, transfer-encoding, header controls, ambiguous Host/Content-Length and canonical path violations fail before application execution.

Only absent or one identity Content-Encoding is supported. Encoded requests return 415; duplicate encoding fields return 400. Encoded/duplicate-encoding guest responses return 502. There is no decompression, expansion allowance, compression worker or implicit gzip/Brotli fallback. Immutable assets retain their identity-only negotiation and range profile.

Hydration and application responsibilities​

The executable reference implementation is examples/browser-boundary/shared/hydration.ts, used by the paired controlled server/client fixture. It transfers only an explicitly selected flat DTO:

Limit or ruleSupported profile
FieldsAt most 64 unique, closed ASCII names, 1-64 characters
ValuesString, finite number other than negative zero, boolean or null
Individual stringAt most 8,192 UTF-16 code units
Complete serialized dataAt most 32,768 UTF-8 bytes, including escaping
Raw-text escaping<, >, &, U+2028 and U+2029 become JSON Unicode escapes
Objects/hooksNo nested objects/arrays, accessors, symbols, toJSON or prototype keys
Client parseBounded input and exact canonical re-encoding, rejecting duplicates/aliases

Explicit projection reads only selected own data properties, not secret getters, and produces a frozen null-prototype DTO. Sorted JSON lives in an inert script[type=application/json]; an external bootstrap reads it and Angular interpolates text without innerHTML or trust-bypass APIs. Both ends disable Angular's automatic HTTP transfer cache with withNoHttpTransferCache(). Angular's own hydration metadata is separate from the application DTO ceiling and remains covered by the renderer's complete output bound.

This is not automatic secret recognition or a JavaScript sandbox. Trusted application code must explicitly decide which scalar values are public; selecting a secret string would disclose it. Do not pass arbitrary server contexts, credentials, provider responses or malicious Proxy objects to serialization. Source-role separation and this fixture's public projection demonstrate exclusion of synthetic bearer/secret/server-object values, not universal information-flow enforcement on arbitrary guest programs. Never enable broad HTTP transfer caching for personalized/provider responses or import server modules into a client bundle.

The platform cannot sanitize arbitrary application HTML or make a deliberately malicious same-origin script safe. Such a script already has that origin's authority. Application authors own template correctness, disclosure decisions, session authentication/rotation, unsafe-method semantics and any URL choices inside their HTML. Operators own TLS, separate tenant origins, proxy-peer isolation, credentials, publication policy and upstream abuse controls.

Cache and authority separation​

The bounded response cache remains an operator-approved immutable-public optimization, not a browser/session cache. It is off by default. Credential-bearing, Cookie, query, conditional and other unapproved application-visible headers bypass it; Origin/Fetch Metadata are not new approved Vary fields. Typical browser requests therefore conservatively bypass that cache. This change neither broadens cache eligibility nor introduces an unbounded tenant/origin map. Dynamic wire responses remain no-store.

The real component regression fills a public entry, returns distinct Alice/Bob cookie-personalized bytes without Age/cache reuse even when the guest asks for public caching, then retrieves the untouched public entry. Cross-tenant origin bindings and credential rejection run before route/asset lookup. A separate regression rejects unsafe HTML before a staged response-cache fill can publish.

Immutable asset caching remains private, immutable and varied on Authorization and Accept-Encoding. Current publication/policy authority is checked on every origin request, including hits/HEAD/304. Browser-cached immutable bytes cannot be recalled by revocation. Neither kind of cached byte buffer grants admission.

Finite abuse evidence and qualification limits​

The real-socket abuse tests keep two incomplete peers within a two-connection profile, serve an eligible asset while one slot is free, reject excess residency, reclaim trickling/silent peers at the original deadline, and then serve another eligible request. No renderer activation is created. Existing TLS handshake, body, idle, write, connection-age and cleanup deadlines remain independently tested. These observations do not prove fairness against a continuous reconnect flood, whole-process RSS isolation or protection from a hostile host/kernel. Loopback RPC bearer authentication and browser HTTP origin/CSRF policy are different boundaries; locality is not tenant identity.

The maintained browser test launches real Chromium against the actual shared HTTP owner and verified immutable asset path, with no network response mocking. It checks original DOM identity, a working Angular click binding, full-document navigation and rehydration, malicious-string round-trip, actual inline/remote CSP violations, base-override rejection, wrong-script-MIME rejection and a same-origin POST reaching the asset method policy. Build/browser receipts are small version/hash/boolean observations and contain no real credentials.

The fixture uses pinned Angular 22.1.7 and Node 24.19.0 to produce controlled Node SSR HTML; it does not claim Wasm-component SSR execution, browser login, production deployment or malicious-application isolation. The asset test authority is explicitly non-cryptographic; existing signature/admission tests own that boundary. Container-root Chromium uses --no-sandbox only in the test harness, so this is not evidence of Chromium's OS sandbox. Parent #236 owns the complete application/SSR release integration after the #226 management/T1 work.

Reproduce the focused checks​

Use the repository's pinned toolchains and a Linux Chromium executable:

cargo test -p latent-ingress --locked
cargo test -p latentd --lib --locked standalone::http
node --test tools/tests/browser_hydration.test.mjs
npm ci --prefix examples/renderer-profile --ignore-scripts --no-audit --no-fund
node tools/browser-boundary/build.mjs examples/renderer-profile /tmp/browser-boundary
LSF_BROWSER_BUILD=/tmp/browser-boundary LSF_BROWSER_NODE="$(command -v node)" LSF_BROWSER_CHROME="$(command -v chromium)" LSF_BROWSER_TOOLCHAIN="$PWD/examples/renderer-profile" cargo test -p latentd --lib --locked actual_browser_boundary -- --ignored --nocapture --test-threads=1

tools/validate_contracts.sh builds the public web component and explicitly runs the actual_http_component tests, including personalized cache and unsafe-output coverage. Ordinary test output marks missing component/browser prerequisites as ignored, not successful execution. CI runs the browser probe from its current Cargo artifact inventory and retains the compact observations; a missing test, browser, fixture or receipt is a failure, not substituted security evidence.

See the bounded local validation observations for exact tested code, dependency heads, counts and redacted artifact identities.