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
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.
Audit metadata
07be5be7ce69bea0c3118744ab90d148b010fce0(origin/mainon 2026-07-17)Dependencies
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-115classifies only literalhost text and intentionally permits loopback for pane navigation.
services/agent-runtime/src/http/browser-handlers.ts:130-136sanitizes theinitial navigation only.
services/agent-runtime/src/browser-host/browser-host.ts:121-140callsPage.navigateand trusts the resulting page URL; redirects and subresourcesare unrestricted.
services/agent-runtime/src/browser-host/reader.ts:130-221resolves everypublic 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
npm --prefix services/agent-runtime test -- browser-network-policynpm --prefix services/agent-runtime run checknpm run test:integrationnpm run checknpm --prefix frontend run desktop:distScope
In scope:
shared/agent/sanitize-embedded-browser-url.tsservices/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.tsservices/agent-runtime/src/browser-host/hosted-page.tsservices/agent-runtime/src/browser-host/cdp.tsandchrome.tsonly as neededfor request interception or a local pinning proxy
services/agent-runtime/src/browser-host/reader.tsservices/agent-runtime/src/http/browser-handlers.tsOut of scope:
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.
publicmode allows only public addresses for documents,redirects, frames, fetch/XHR, WebSockets, workers, and subresources. Explicit
loopbackmode allows loopback plus public destinations but never LAN/link-localor 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.tsstill consumes the shared classification/resolution contractfrom 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:distand perform theexact canonical
AGENTS.mdreinstall: replace/Applications/Local Studio.appwith the
mac-arm64output viarm -rfplusditto, remove the legacy user app,terminate/re-register/relaunch Local Studio, and query
/api/desktop-healthonthe 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 withok: truefrom/api/desktop-health.Test plan
.localhost, LAN, link-local, metadata-like, mappedIPv6, mixed DNS, no answers, timeout, and resolver error.
allowed; loopback-to-LAN blocked.
Host/SNI and certificate validation.
Acceptance criteria
Risks and stop conditions
Stop and report if:
pinning proxy is implemented.
explicit capability and threat review.
/api/desktop-healthverification 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.