From 8da666c12d9432c449e4f28dae1b0769713b184a Mon Sep 17 00:00:00 2001 From: SarthakWade Date: Wed, 12 Aug 2026 20:39:04 +0530 Subject: [PATCH] fix(macos): scope app transport security --- CHANGELOG.md | 2 ++ SECURITY.md | 29 +++++++++++--------- apps/headless/Tests/macos-bundle-security.sh | 23 ++++++++++++++++ apps/headless/build.sh | 2 +- docs/roadmap/improvements-backlog.md | 17 +++++++----- 5 files changed, 52 insertions(+), 21 deletions(-) create mode 100755 apps/headless/Tests/macos-bundle-security.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a6f257..5100153 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -94,6 +94,8 @@ Cutting that release is tracked in ### Fixed +- The macOS app no longer disables App Transport Security process-wide. Its + HTTP compatibility exception is limited to browser web content. - WebKit and Chromium now agree on empty-history `back` failures and enforce the same bounded key input before dispatch. - Portable name characters, local-development hosts, scroll bounds, and diff --git a/SECURITY.md b/SECURITY.md index 1214ff6..4419d95 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,6 +1,6 @@ # Security policy -Headless exists to give an AI agent a browser it *cannot* misuse. The safety +Headless exists to give an AI agent a browser it _cannot_ misuse. The safety rules are enforced by the host process, not by prompting, so a vulnerability here is a vulnerability in the product's core promise. We take reports seriously. @@ -28,18 +28,18 @@ we will agree a timeline with you. These are host-enforced contracts. Anything that defeats one is in scope: -| Boundary | Expected behaviour | -| --- | --- | -| **No arbitrary code execution** | There is no JavaScript-evaluation verb and no shell verb. Reaching arbitrary in-page or host execution through the protocol is a vulnerability. | -| **Navigation** | HTTP/HTTPS only. `file:`, `javascript:`, `data:`, credential-bearing URLs, and external application schemes must be refused at every layer. | -| **Downloads** | Page-initiated downloads are denied. Executables, installers, scripts, libraries, and disk images are blocked by extension. | -| **Control plane** | A `0600` Unix socket inside a `0700` per-user directory, with a peer-UID check. There is no TCP listener and no Chromium debug port. Any remote reachability is a vulnerability. | -| **Artifacts** | Bare validated names, `O_EXCL` creation at `0600` inside a `0700` root, never overwritten. Path traversal or reading outside the store is a vulnerability. | -| **Secrets** | Cookie and storage *values* require both `--values` and `HEADLESS_ALLOW_SENSITIVE_DIAGNOSTICS=1`. Authorization, cookie, token, and secret headers, plus URL credentials, are always redacted. Flow recordings never contain typed values. | -| **Untrusted content** | Everything derived from a page is marked `untrustedContent` and is never executed as a command. A page that induces the host to act on its own text is a vulnerability. | -| **Sandbox** | The Linux host refuses to run as root and never passes `--no-sandbox`. Snap Chromium is rejected before launch. | - -Prompt injection that merely *persuades an agent* to do something within these +| Boundary | Expected behaviour | +| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| **No arbitrary code execution** | There is no JavaScript-evaluation verb and no shell verb. Reaching arbitrary in-page or host execution through the protocol is a vulnerability. | +| **Navigation** | HTTP/HTTPS only. `file:`, `javascript:`, `data:`, credential-bearing URLs, and external application schemes must be refused at every layer. | +| **Downloads** | Page-initiated downloads are denied. Executables, installers, scripts, libraries, and disk images are blocked by extension. | +| **Control plane** | A `0600` Unix socket inside a `0700` per-user directory, with a peer-UID check. There is no TCP listener and no Chromium debug port. Any remote reachability is a vulnerability. | +| **Artifacts** | Bare validated names, `O_EXCL` creation at `0600` inside a `0700` root, never overwritten. Path traversal or reading outside the store is a vulnerability. | +| **Secrets** | Cookie and storage _values_ require both `--values` and `HEADLESS_ALLOW_SENSITIVE_DIAGNOSTICS=1`. Authorization, cookie, token, and secret headers, plus URL credentials, are always redacted. Flow recordings never contain typed values. | +| **Untrusted content** | Everything derived from a page is marked `untrustedContent` and is never executed as a command. A page that induces the host to act on its own text is a vulnerability. | +| **Sandbox** | The Linux host refuses to run as root and never passes `--no-sandbox`. Snap Chromium is rejected before launch. | + +Prompt injection that merely _persuades an agent_ to do something within these boundaries is not a host vulnerability — but if page content can escape the `untrustedContent` marking or reach a privileged path, that is. @@ -61,6 +61,9 @@ These are documented design boundaries, not defects: chrome, other applications, or audio. - **Network mocking is Linux-only.** macOS returns `UNSUPPORTED_CAPABILITY` rather than partially emulating traffic control. +- **HTTP on macOS.** The ATS exception is limited to `WKWebView` so browser + pages can use HTTP when required. Native application networking retains the + default ATS protections. ## Supported versions diff --git a/apps/headless/Tests/macos-bundle-security.sh b/apps/headless/Tests/macos-bundle-security.sh new file mode 100755 index 0000000..cd4751c --- /dev/null +++ b/apps/headless/Tests/macos-bundle-security.sh @@ -0,0 +1,23 @@ +#!/bin/sh +set -eu + +PLIST="${1:?usage: macos-bundle-security.sh /path/to/Info.plist}" + +fail() { + echo "macOS bundle security: $1" >&2 + exit 1 +} + +command -v plutil >/dev/null 2>&1 || fail "plutil is required" +plutil -lint "$PLIST" >/dev/null || fail "Info.plist is invalid" + +if plutil -extract NSAppTransportSecurity.NSAllowsArbitraryLoads raw "$PLIST" >/dev/null 2>&1; then + fail "NSAllowsArbitraryLoads must not disable ATS for the entire app" +fi + +WEB_CONTENT_EXCEPTION="$( + plutil -extract NSAppTransportSecurity.NSAllowsArbitraryLoadsInWebContent raw "$PLIST" 2>/dev/null +)" || fail "the WKWebView-scoped ATS exception is missing" +test "$WEB_CONTENT_EXCEPTION" = "true" || fail "the WKWebView-scoped ATS exception must be true" + +echo "macOS bundle ATS configuration passed" diff --git a/apps/headless/build.sh b/apps/headless/build.sh index b6d0a50..8f056fa 100755 --- a/apps/headless/build.sh +++ b/apps/headless/build.sh @@ -94,13 +94,13 @@ cat > "$APP/Contents/Info.plist" <NSSupportsAutomaticGraphicsSwitching NSAppTransportSecurity - NSAllowsArbitraryLoads NSAllowsArbitraryLoadsInWebContent NSHumanReadableCopyrightheadless — the browser that isn’t there PLIST +Tests/macos-bundle-security.sh "$APP/Contents/Info.plist" # Passkeys require Apple's restricted web-browser.public-key-credential # entitlement backed by a provisioning profile; macOS SIGKILLs ad-hoc builds diff --git a/docs/roadmap/improvements-backlog.md b/docs/roadmap/improvements-backlog.md index 766d6f2..ef3eceb 100644 --- a/docs/roadmap/improvements-backlog.md +++ b/docs/roadmap/improvements-backlog.md @@ -74,9 +74,9 @@ burning a core while silently refusing every agent. map is reset on each `snapshot()` (`HP/AgentRuntime.swift:376`), so a `--context summary` (max 8 elements) invalidates all refs from a prior `full`; the agent later gets a bare `ELEMENT_NOT_FOUND`. Meanwhile -`currentRegions` is *never* reset and grows for the page lifetime. ~~Decide the +`currentRegions` is _never_ reset and grows for the page lifetime. ~~Decide the contract (likely: refs from the latest inspection only — already the skill's -teaching), then (a) make the error say *why* ("ref expired; re-inspect"), +teaching), then (a) make the error say _why_ ("ref expired; re-inspect"), (b) reset regions consistently on navigation, (c) document in P1.md. Test: inspect-full → inspect-summary → click stale `@eN` asserts the new error.~~ **Done.** The contract is now explicit and asymmetric on purpose: `@eN` is @@ -115,6 +115,7 @@ message buffer scans each appended region once and amortizes prefix compaction; protocol coverage feeds it a 30 MiB message in the host's 8 KiB read chunks. **A9. Misc hardening (smaller, same phase).** ([#20](https://github.com/LockInTime/headless/issues/20)) + - ~~`ChromiumChildProcess.stop()` can busy-wait forever post-SIGKILL (`LinuxHost/BrowserProcess.swift:38`); bound it.~~ - ~~`SO_PEERCRED` hard-coded as `17` + hand-rolled `ucred` @@ -198,7 +199,7 @@ branch, and hid the backward-compatible no-op `--json` parser flag from help. **B5. `pruneToBudget` quality.** ([#25](https://github.com/LockInTime/headless/issues/25)) ~~Hand-rolled 2-pass fixed point (`HP/AgentRuntime.swift:348-352`), O(n²) re-encoding per trim, pop-largest- -*last*-element heuristic misses large mid-array items +_last_-element heuristic misses large mid-array items (`AgentRuntime.swift:367-369`), text-chop fallback untested. Rework with a size-estimating single pass; add unit tests in the jsdom suite.~~ **Done:** each candidate is measured once, largest entries are pruned regardless of @@ -269,7 +270,7 @@ oversized line / local-command rejection (`MCP/main.swift:64-66`).~~ **Done:** uses a private local socket server to verify a real browser-command round trip. **C4. Machine-accurate `capabilities`** ([#32](https://github.com/LockInTime/headless/issues/32)) — generate from `CommandName.allCases` -+ engine matrix (see B6) so agents can trust it. +and the engine matrix (see B6) so agents can trust it. **C5. Harness onboarding [exists: skill content].** ([#33](https://github.com/LockInTime/headless/issues/33)) Root `AGENTS.md` + `CLAUDE.md` (added with this doc set); mirror the skill into `.claude/skills/` @@ -376,12 +377,14 @@ Owner-decided scope: package managers, no hosted service. default `HEADLESS_VERSION` is 1.0.0. - **E7.** ([#45](https://github.com/LockInTime/headless/issues/45)) Cut a release: everything since v1.0.2 (capture formats, context pruning) is unreleased. -- **E8.** ([#46](https://github.com/LockInTime/headless/issues/46)) `NSAllowsArbitraryLoads` is blanket-true (`build.sh:86-90`); scope - it (localhost exception) if WKWebView allows. +- **E8.** [x] ([#46](https://github.com/LockInTime/headless/issues/46)) ~~`NSAllowsArbitraryLoads` is blanket-true + (`build.sh:86-90`); scope it (localhost exception) if WKWebView allows.~~ + **Done:** native app networking retains ATS while the HTTP compatibility + exception is limited to `WKWebView`; the bundle build asserts this boundary. ## §F — Website & docs (Phase 5) -- **F1. Deploy pipeline is invisible to the repo** ([#47](https://github.com/LockInTime/headless/issues/47)) — the site *is* live at +- **F1. Deploy pipeline is invisible to the repo** ([#47](https://github.com/LockInTime/headless/issues/47)) — the site _is_ live at `https://headless-web-pi.vercel.app` (set as the repo homepage) via Vercel's GitHub integration, but nothing in the tree records that: no `vercel.json`, no deploy docs, no preview-URL comment on PRs, and the temporary