Skip to content

feat(security): add host origin allowlist on start - #170

Merged
SarthakWade merged 6 commits into
mainfrom
feat/navigation-allowlist
Sep 12, 2026
Merged

feat(security): add host origin allowlist on start#170
SarthakWade merged 6 commits into
mainfrom
feat/navigation-allowlist

Conversation

@yashranaway

Copy link
Copy Markdown
Collaborator

Summary

Adds a host-enforced origin allowlist so a prompt-injected agent cannot leave the app under test. headless start --allow PATTERN restricts visit, top-frame navigation, and in-page clicks; omitting --allow keeps today's unrestricted HTTP(S) behavior.

Contract

headless start [--background|--foreground] [--allow PATTERN]...
headless start --allow localhost --allow '*.staging.example.com'
headless start --allow localhost,127.0.0.1
  • Repeatable --allow; comma-separated values in one flag also accepted.
  • Presentation flags stay macOS-only.
  • Still HTTP/HTTPS only. --allow cannot add file:, javascript:, credentials, or blocked extensions. normalizedWebURL is unchanged; the allowlist is an extra conjunct in agentMayNavigate.
  • Empty --allow is a parse error. * alone, paths, schemes, credentials, non-ASCII, and unexpected characters fail closed. Cap 32 patterns; case-insensitive DNS; IPv4 literals as exact hosts.
  • *.example.com matches subdomains, not the apex, and not example.com.evil.test.
  • localhost matches any port; localhost:3000 matches only that port.
  • When set, a non-matching host fails with UNSAFE_NAVIGATION.
  • status / ping includes navigationAllowlist (empty array means unrestricted).
  • Changing the list on a running host is rejected (NAVIGATION_ALLOWLIST_CONFLICT — stop first). Matching list or start without --allow against a running host is a no-op.
  • CLI sets HEADLESS_NAVIGATION_ALLOWLIST on the spawned host. The injected runtime gets a JSON-encoded copy as defense in depth; host policy is authoritative.
  • Protocol version stays 0.5 (additive ping field). Architecture decision 22.

Tests

  • Protocol suite: parse (repeated, comma-separated, with presentation flags), reject * / schemes / credentials / empty --allow / too many patterns, matcher (wildcard non-match, apex vs subdomain, port exactness, unrestricted when empty), visit still validates example.com as a URL, host-side deny via agentMayNavigate, ping reports the list.
  • Agent runtime: click guard honors __headlessNavigationAllowlist.
  • Linux E2E: stop, start --allow 127.0.0.1, fixture visit OK, https://example.com and off-list click fail UNSAFE_NAVIGATION, mismatch requires stop, restore unrestricted host.
  • macOS E2E: matching restart block.

pnpm test:runtime passed. Local pnpm test cannot run Swift on this Linux box (exit 69); the Linux Docker builder ran 46/46 protocol tests and Linux P2 E2E passed, including the new allowlist block.

Review

@SarthakWade please review before morning.

Closes #167

@yashranaway yashranaway self-assigned this Sep 10, 2026
@yashranaway

Copy link
Copy Markdown
Collaborator Author

Please review this one first. #169 (file upload) was branched from the same main and will need a rebase after this merges. They share CLI/protocol/docs files but the allowlist contract does not depend on upload.

@yashranaway

Copy link
Copy Markdown
Collaborator Author

macOS CI failed on the runner SDK, not the allowlist: no macOS SDK compatible with the installed Swift compiler. Linux protocol + E2E are green. Reran the failed job.

@yashranaway

Copy link
Copy Markdown
Collaborator Author

The macOS failure was the SDK probe, not the runner. build.sh typechecks Protocol.swift alone to pick an SDK. agentMayNavigate now references NavigationAllowlist, so every SDK failed that probe and the script said the toolchain was missing. Probe now includes NavigationAllowlist.swift.

Restrict agent navigation with repeatable `headless start --allow PATTERN`.
The matcher is an extra conjunct in agentMayNavigate; ping reports the
active list, and changing it on a running host requires stop first.
agentMayNavigate now references NavigationAllowlist. The macOS SDK
selection typechecks Protocol.swift in isolation, so every SDK looked
incompatible and CI reported a missing toolchain.
@yashranaway
yashranaway force-pushed the feat/navigation-allowlist branch from be5f0cd to 703bd93 Compare September 12, 2026 05:27
@yashranaway

Copy link
Copy Markdown
Collaborator Author

Rebased onto current main. Mergeable. Keep this ahead of #169 if both land; they still overlap on protocol files.

@SarthakWade SarthakWade left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The allowlist contract is not yet enforced across every claimed path, so this should not merge yet.

The main blocker is Linux navigation triggered by a non-anchor control. The isolated runtime only preflights HTML anchors. A form submit button or trusted click handler that assigns location is allowed to execute, then the existing Page.frameRequestedNavigation recovery runs asynchronously. That cannot make the click fail with UNSAFE_NAVIGATION and does not guarantee an off-list request was never sent. A trusted click can also open a new Chromium target, and the browser process does not auto-attach that target to the session enforcement path. Please enforce the policy before request dispatch or at target creation, then cover form submission, script-driven top-frame navigation, redirects, and new-window attempts in Linux E2E.

There are two launcher correctness gaps as well: the post-spawn polling loop returns the first successful ping without checking that it carries the requested allowlist, and list comparison is order-sensitive even though pattern order has no semantic meaning.

This PR touches the macOS host and adds macOS E2E assertions, but the WKWebView E2E job was skipped. Please run it before merge as required by the repository contract.

GitHub currently reports the PR as mergeable with no conflicts. All required branch checks are green, and I also ran pnpm test (60/60 protocol tests) plus pnpm test:runtime locally. The separate linux-arm64 Release check failed during an existing authentication-storage-state E2E with a closed DevTools pipe; it is not a required branch check, but should be rerun or confirmed as unrelated.

Comment thread apps/headless/Sources/HeadlessProtocol/Resources/AgentRuntime.js Outdated
Comment thread apps/headless/Sources/HeadlessCLI/main.swift
Comment thread apps/headless/Sources/HeadlessCLI/main.swift Outdated
Fail off-list Document navigations at Fetch.requestPaused, close extra
page targets, and revalidate every start ping as a set.
@yashranaway yashranaway added the macos-e2e Run the macOS WKWebView E2E suite label Sep 12, 2026
@yashranaway

Copy link
Copy Markdown
Collaborator Author

Addressed the CHANGES_REQUESTED review.

P1 requireSafeClickTarget (AgentRuntime.js)
Click preflight now also fails UNSAFE_NAVIGATION for submit controls (HTMLButtonElement / input type=submit|image / HTMLFormElement) using formaction, then form.action, then the document URL. That cannot see onclick that assigns location or calls window.open.

Linux is the real boundary: when the allowlist is restricted, Fetch is enabled for Document Request (merged with mock URL patterns, never dropped when mocks change). Fetch.requestPaused fulfills mocks as before; off-list Document requests get Fetch.failRequest (BlockedByClient) plus a navigation-blocked diagnostic and are not continued. Extra page targets use Target.setAutoAttach (waitForDebuggerOnStart, flatten); they are closed and not inserted into sessionsByProtocolID. macOS already cancels in decidePolicyFor and ignores disallowed createWebViewWith URLs.

E2E on linux and macos now covers form submit, script navigation, window.open, and a same-origin redirect while the host is started with --allow 127.0.0.1. Form submit must fail UNSAFE_NAVIGATION. Script/window/redirect must not leave 127.0.0.1.

P1 start ping revalidation (HeadlessCLI)
The same allowlist check runs on every successful ping, including the post-process.run loop. A mismatch after spawn terminates the process we started.

P2 allowlist order
Comparison is Set equality, so start --allow 127.0.0.1 --allow localhost then start --allow localhost --allow 127.0.0.1 is a no-op. Protocol test covers swapped parse order as sets. E2E covers the reordered start.

Also added the macos-e2e label.

Target.attachedToTarget also fires for attachToTarget. Closing extra
page targets in that path would kill the agent session when no
allowlist is set.
waitForDebuggerOnStart pauses window.open targets. Closing that
session without Runtime.runIfWaitingForDebugger wedged the DevTools
pipe, so inspect after an off-list popup returned HOST_UNAVAILABLE.
@yashranaway

Copy link
Copy Markdown
Collaborator Author

Follow-up for the Linux E2E failure after window.open. Auto-attached popup targets were left paused at waitForDebuggerOnStart, so Target.closeTarget never ran and the host stopped answering inspect. Those sessions now get Runtime.runIfWaitingForDebugger before close.

@SarthakWade SarthakWade left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The allowlist enforcement fixes and stable session-state assertions have been re-reviewed. Protocol, runtime, Linux Docker E2E, and macOS WKWebView E2E all pass on the latest head.

@SarthakWade
SarthakWade merged commit 75f1315 into main Sep 12, 2026
16 checks passed
@SarthakWade
SarthakWade deleted the feat/navigation-allowlist branch September 12, 2026 11:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

macos-e2e Run the macOS WKWebView E2E suite

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Host origin allowlist on start

2 participants