Skip to content

[Security] Enforce DNS-pinned browser policy across redirects and subresources #235

Description

@fettpl

Audit metadata

  • Priority: P1
  • Estimated effort: L
  • Implementation risk: HIGH
  • Category: security
  • Evidence baseline: 07be5be7ce69bea0c3118744ab90d148b010fce0 (origin/main on 2026-07-17)

Dependencies

  • Audited P2 proposal 015 — Make degraded browser mode functional and self-throttling. Not filed in this P1-only wave.
  • Audited P2 proposal 028 — Make the standalone agent runtime independently installable and verified. Not filed in this P1-only wave.

Description and impact

The browser pane accepts a hostname after lexical classification, then Chromium
performs DNS, redirects, and subresource requests without an address-level
policy. A public name can resolve or rebind to loopback/private infrastructure,
and a public page can request local services. The separate reading-mode client
already validates and pins DNS per redirect, but the interactive browser does
not share that protection.

Current state

  • shared/agent/sanitize-embedded-browser-url.ts:85-115 classifies only literal
    host text and intentionally permits loopback for pane navigation.
  • services/agent-runtime/src/http/browser-handlers.ts:130-136 sanitizes the
    initial navigation only.
  • services/agent-runtime/src/browser-host/browser-host.ts:121-140 calls
    Page.navigate and trusts the resulting page URL; redirects and subresources
    are unrestricted.
  • services/agent-runtime/src/browser-host/reader.ts:130-221 resolves every
    public host, rejects any private answer, revalidates redirects, and pins the
    selected address into the HTTP request. Reuse its principles, not its global
    test hooks.

Verification commands

Purpose Command Expected on success
Network-policy tests npm --prefix services/agent-runtime test -- browser-network-policy all address/redirect/resource cases pass
Runtime check npm --prefix services/agent-runtime run check exit 0
Regression npm run test:integration browser regressions pass
Repository gate npm run check exit 0
Desktop package npm --prefix frontend run desktop:dist production desktop bundle created

Scope

In scope:

  • shared/agent/sanitize-embedded-browser-url.ts
  • services/agent-runtime/src/browser-host/network-policy.ts (create)
  • services/agent-runtime/src/browser-host/network-policy.test.ts (create)
  • services/agent-runtime/src/browser-host/browser-host.ts
  • services/agent-runtime/src/browser-host/hosted-page.ts
  • services/agent-runtime/src/browser-host/cdp.ts and chrome.ts only as needed
    for request interception or a local pinning proxy
  • services/agent-runtime/src/browser-host/reader.ts
  • services/agent-runtime/src/http/browser-handlers.ts
  • Focused browser integration/regression tests

Out of scope:

  • Permitting LAN/private ranges for public browsing.
  • File URLs, arbitrary proxy settings, credential injection, or disabling TLS.
  • UI redesign or browser session isolation; Plans 041-042 own those.
  • Shipping a partial policy that protects only top-level navigation.

Solution design

Extend audit proposal 015's injectable reader/pane policy into one browser-wide policy
that normalizes IPv4/IPv6 (including mapped forms),
resolves every hostname, rejects mixed or disallowed answers, and returns a
navigation mode. public mode allows only public addresses for documents,
redirects, frames, fetch/XHR, WebSockets, workers, and subresources. Explicit
loopback mode allows loopback plus public destinations but never LAN/link-local
or other private ranges. Revalidate each redirect/request. Install interception
before navigation and disable/bypass service-worker paths that escape it. If
Chromium can still perform its own unpinned DNS after validation, route all HTTP,
HTTPS, and WebSocket traffic through a local proxy that resolves once, connects
to the approved address, preserves Host/SNI and TLS verification, and refuses
unapproved CONNECT targets.

Implementation plan

Step 1: Extend the shared address policy

Start from audit proposal 015's typed public-versus-pane reader policy. Move any remaining
literal/address classification into its pure module, retain the injected DNS
resolver and Effect schema validation, and define public, loopback, and blocked
classes for all special-use IPv4/IPv6 ranges. Fail closed on zero or mixed
answers; do not create a parallel policy abstraction.

Verify: table tests cover loopback, RFC1918, link-local, CGNAT, multicast,
documentation ranges as policy specifies, IPv4-mapped IPv6, zone IDs, malformed,
mixed, and public answers.

Step 2: Intercept every Chromium request before connection

Enable CDP Fetch/Network policy before any page navigation, including frames,
redirects, subresources, fetch/XHR, WebSocket/EventSource, workers, prefetch, and
downloads. Keep the initiating top-level mode for the page lifetime. Abort with
a stable blocked error and never fall through on resolver failure.

Verify: integration fixtures assert a disallowed destination server records
zero accepted connections for direct, redirect, image/script, iframe, fetch,
WebSocket, and worker attempts.

Step 3: Close DNS rebinding and service-worker gaps

Test a resolver that returns public during policy evaluation and private during
Chromium resolution. If CDP cannot prove address pinning, introduce the bounded
local proxy and configure the managed Chromium process to use it with no bypass
except its CDP control endpoint. Validate TLS normally; never disable certificate
checks.

Verify: rebinding test reaches only the policy-selected test address, and a
service-worker/subresource test cannot connect to the blocked server.

Step 4: Make reading and interactive modes share policy semantics

Confirm reader.ts still consumes the shared classification/resolution contract
from audit proposal 015 while retaining its byte/redirect/time limits. Pane fallback must
support explicit loopback only in loopback mode and keep public mode public-only.

Verify: the same URL/address fixture table produces identical allow/deny
decisions in reader and Chromium paths.

Step 5: Run security gates and verify the packaged desktop

Run focused zero-connection tests, runtime checks, and all regressions. Because
the agent runtime is bundled into Electron, build desktop:dist and perform the
exact canonical AGENTS.md reinstall: replace /Applications/Local Studio.app
with the mac-arm64 output via rm -rf plus ditto, remove the legacy user app,
terminate/re-register/relaunch Local Studio, and query /api/desktop-health on
the persisted embedded port.

Verify: npm --prefix services/agent-runtime run check && npm run check && npm run test:integration && npm --prefix frontend run desktop:dist && git diff --check → all exit 0; the reinstalled canonical app returns HTTP 200 with ok: true from /api/desktop-health.

Test plan

  • Public, literal loopback, .localhost, LAN, link-local, metadata-like, mapped
    IPv6, mixed DNS, no answers, timeout, and resolver error.
  • Public-to-loopback/private redirect blocked; explicit loopback-to-public
    allowed; loopback-to-LAN blocked.
  • Image/script/font/iframe/fetch/XHR/WebSocket/worker/service-worker paths.
  • DNS rebinding between policy and connection.
  • Blocked fixture records exactly zero connections; allowed fixture preserves
    Host/SNI and certificate validation.
  • Navigation mode cannot be upgraded by redirect or page script.

Acceptance criteria

  • One policy controls reader and interactive browsing.
  • Every hostname is resolved and every address classified before connection.
  • Redirects and all relevant subresources retain the top-level mode.
  • Public pages cannot reach loopback, LAN, link-local, or private services.
  • Explicit loopback pages can reach loopback/public only.
  • DNS rebinding and service-worker bypass tests prove zero blocked connections.
  • TLS verification remains enabled.
  • Runtime, repository, and integration gates pass.
  • The canonical desktop app is rebuilt, reinstalled, relaunched, and healthy.

Risks and stop conditions

Stop and report if:

  • CDP cannot intercept a request class before network connection and no safe
    pinning proxy is implemented.
  • Address pinning requires disabling TLS verification or trusting arbitrary certs.
  • Chromium proxy bypass rules expose loopback/private destinations.
  • A supported workflow intentionally needs LAN access; that requires a separate
    explicit capability and threat review.
  • The canonical desktop reinstall or /api/desktop-health verification fails.

Maintenance considerations

Special-use address tables and request classes are security-sensitive; add tests
before changing them. Reviewers should demand zero-connection evidence, not only
an error response. audit proposal 041 should reuse this protected CDP surface everywhere.


Generated from Local Studio 2.0 main-branch audit proposal 040.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions