Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,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
Expand Down
29 changes: 16 additions & 13 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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.

Expand All @@ -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

Expand Down
23 changes: 23 additions & 0 deletions apps/headless/Tests/macos-bundle-security.sh
Original file line number Diff line number Diff line change
@@ -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"
2 changes: 1 addition & 1 deletion apps/headless/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -94,13 +94,13 @@ cat > "$APP/Contents/Info.plist" <<PLIST
<key>NSSupportsAutomaticGraphicsSwitching</key><true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key><true/>
<key>NSAllowsArbitraryLoadsInWebContent</key><true/>
</dict>
<key>NSHumanReadableCopyright</key><string>headless — the browser that isn’t there</string>
</dict>
</plist>
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
Expand Down
6 changes: 4 additions & 2 deletions docs/roadmap/improvements-backlog.md
Original file line number Diff line number Diff line change
Expand Up @@ -380,8 +380,10 @@ 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)

Expand Down
Loading