From 4d25ecc1d9b96dfbb2f3a4a5b9e0cc4602bd6c7a Mon Sep 17 00:00:00 2001
From: Aditya Garud <153842990+yashranaway@users.noreply.github.com>
Date: Mon, 10 Aug 2026 18:57:19 +0000
Subject: [PATCH 1/4] test(security): cover peer and host denials
---
CHANGELOG.md | 2 +
apps/headless/Tests/Fixtures/dashboard.html | 3 +-
.../HeadlessProtocolTests/ProtocolTests.swift | 61 +++++++++++++++++++
apps/headless/Tests/fixture-server.mjs | 11 ++++
apps/headless/Tests/linux-e2e.sh | 4 ++
apps/headless/Tests/macos-e2e.sh | 6 ++
docs/roadmap/improvements-backlog.md | 5 +-
7 files changed, 90 insertions(+), 2 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 518ce9d..f93105b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -36,6 +36,8 @@ Cutting that release is tracked in
- Recording and transport regression coverage for every ffmpeg format/quality
mapping, executable discovery, capture-failure and stop bounds, capabilities
accuracy, and oversized Unix-socket requests.
+- Cross-uid Unix-socket rejection and host E2E classification checks for
+ blocked top-frame navigation and denied page-initiated downloads.
- Progressive context pruning: `inspect --context summary|outline|text|actions|full`
with `--task` ranking, `--within @rN` scoping, and `--limit` / `--budget` /
`--depth` bounds. Every focused response reports `contextStats` and `omitted`.
diff --git a/apps/headless/Tests/Fixtures/dashboard.html b/apps/headless/Tests/Fixtures/dashboard.html
index 8e3cb0e..6b0977e 100644
--- a/apps/headless/Tests/Fixtures/dashboard.html
+++ b/apps/headless/Tests/Fixtures/dashboard.html
@@ -30,7 +30,8 @@
Continue setup
Non-web browser URL
Credential-bearing URL
Suspicious installer
-
+ Download fixture
+
diff --git a/apps/headless/Tests/HeadlessProtocolTests/ProtocolTests.swift b/apps/headless/Tests/HeadlessProtocolTests/ProtocolTests.swift
index 48fb01f..ff5ee8a 100644
--- a/apps/headless/Tests/HeadlessProtocolTests/ProtocolTests.swift
+++ b/apps/headless/Tests/HeadlessProtocolTests/ProtocolTests.swift
@@ -1095,6 +1095,48 @@ struct ProtocolTests {
try expect(response.result == nil, "oversized request must not reach the command handler")
}
+ static func differentPeerUserIsRejected() throws {
+ #if os(Linux)
+ // The Linux CI container runs this suite as root, which lets the test
+ // launch one deliberately unprivileged peer. Normal developer runs
+ // still exercise every other transport boundary without requiring
+ // privilege escalation.
+ guard getuid() == 0 else { return }
+ let setpriv = "/usr/bin/setpriv"
+ try expect(FileManager.default.isExecutableFile(atPath: setpriv), "Linux CI should provide setpriv")
+ try LocalRuntime.preparePrivateDirectory()
+ let socketPath = LocalRuntime.directoryURL
+ .appendingPathComponent("peer-uid-\(UUID().uuidString).sock").path
+ let server = LocalSocketServer(socketPath: socketPath)
+ try server.start { request in CommandResponse.success(id: request.id) }
+ defer {
+ server.stop()
+ _ = Glibc.chmod(LocalRuntime.directoryURL.path, 0o700)
+ }
+ // The production modes are 0700/0600. Open them only inside this
+ // disposable test so a different uid can reach accept(), where the
+ // credential check must still fail closed.
+ try expect(Glibc.chmod(LocalRuntime.directoryURL.path, 0o777) == 0, "test runtime directory chmod failed")
+ try expect(Glibc.chmod(socketPath, 0o666) == 0, "test socket chmod failed")
+
+ let process = Process()
+ process.executableURL = URL(fileURLWithPath: setpriv)
+ process.arguments = [
+ "--reuid=65534", "--regid=65534", "--clear-groups",
+ CommandLine.arguments[0], "--peer-denied-client", socketPath,
+ ]
+ process.standardOutput = FileHandle.nullDevice
+ let errors = Pipe()
+ process.standardError = errors
+ try process.run()
+ process.waitUntilExit()
+ let errorText = String(
+ decoding: errors.fileHandleForReading.readDataToEndOfFile(), as: UTF8.self
+ )
+ try expect(process.terminationStatus == 0, "different-uid peer was not rejected: \(errorText)")
+ #endif
+ }
+
static func screenshotSeriesHelpers() throws {
let rawPlan = JSONValue.object([
"initialY": .number(240),
@@ -1386,6 +1428,24 @@ struct ProtocolTests {
}
static func main() {
+ if CommandLine.arguments.count == 3,
+ CommandLine.arguments[1] == "--peer-denied-client" {
+ do {
+ let descriptor = try connectRawUnixSocket(path: CommandLine.arguments[2])
+ defer { closeRawSocket(descriptor) }
+ let response = try ProtocolCodec.decodeLine(
+ CommandResponse.self, from: readRawSocketLine(descriptor: descriptor)
+ )
+ guard response.error?.code == "PEER_DENIED" else {
+ fputs("expected PEER_DENIED\n", stderr)
+ exit(1)
+ }
+ exit(0)
+ } catch {
+ fputs("peer client failed: \(error)\n", stderr)
+ exit(1)
+ }
+ }
let tests: [TestCase] = [
("request round-trip", requestRoundTrip),
("unsafe navigation schemes", rejectsUnsafeNavigationSchemes),
@@ -1426,6 +1486,7 @@ struct ProtocolTests {
("private socket directory", serverRejectsSocketOutsidePrivateDirectory),
("shutdown bypasses busy request", shutdownBypassesBusyRequest),
("oversized socket request", oversizedSocketRequestIsRejected),
+ ("different peer uid", differentPeerUserIsRejected),
]
var failures = 0
diff --git a/apps/headless/Tests/fixture-server.mjs b/apps/headless/Tests/fixture-server.mjs
index 176fe2f..1847275 100644
--- a/apps/headless/Tests/fixture-server.mjs
+++ b/apps/headless/Tests/fixture-server.mjs
@@ -25,6 +25,17 @@ const server = createServer(async (request, response) => {
response.end(body);
return;
}
+ if (pathname === '/download.txt') {
+ const body = Buffer.from('download must remain blocked');
+ response.writeHead(200, {
+ 'content-type': 'text/plain; charset=utf-8',
+ 'content-length': body.length,
+ 'content-disposition': 'attachment; filename="download.txt"',
+ 'cache-control': 'no-store',
+ });
+ response.end(body);
+ return;
+ }
const fixture = routes.get(pathname);
if (!fixture) {
response.writeHead(404, {'content-type': 'text/plain; charset=utf-8'});
diff --git a/apps/headless/Tests/linux-e2e.sh b/apps/headless/Tests/linux-e2e.sh
index dce6326..f0cf8ae 100755
--- a/apps/headless/Tests/linux-e2e.sh
+++ b/apps/headless/Tests/linux-e2e.sh
@@ -130,6 +130,10 @@ fi
echo "$SUSPICIOUS_RESULT" | grep -q 'UNSAFE_RESOURCE_TYPE'
headless --session qa click --role button --name 'Scripted non-web navigation' | grep -q '"clicked"'
headless --session qa wait --url /designers/dashboard --settled --timeout 10000 | grep -q 'Designers Dashboard'
+BLOCKED_NAVIGATION_REPORT="$(headless --session qa qa report)"
+echo "$BLOCKED_NAVIGATION_REPORT" | grep -q '"kind":"navigation-blocked"'
+echo "$BLOCKED_NAVIGATION_REPORT" | grep -q 'about:blank'
+headless --session qa qa clear | grep -q '"cleared"'
headless --session qa screenshot --output viewport.png | grep -q '"name":"viewport.png"'
headless --session qa screenshot --full-page --output full-page.png | grep -q '"name":"full-page.png"'
headless --session qa screenshot --role button --name Continue --output continue.png | grep -q '"name":"continue.png"'
diff --git a/apps/headless/Tests/macos-e2e.sh b/apps/headless/Tests/macos-e2e.sh
index c26ad9f..4362b47 100755
--- a/apps/headless/Tests/macos-e2e.sh
+++ b/apps/headless/Tests/macos-e2e.sh
@@ -207,6 +207,12 @@ fi
echo "$SUSPICIOUS_RESULT" | grep -q 'UNSAFE_RESOURCE_TYPE'
"$CLI" --session qa click --role button --name 'Scripted non-web navigation' | grep -q '"clicked"'
"$CLI" --session qa wait --url /designers/dashboard --settled --timeout 10000 | grep -q 'Designers Dashboard'
+"$CLI" --session qa click --role link --name 'Download fixture' | grep -q '"clicked"'
+sleep 1
+BLOCKED_DOWNLOAD_REPORT="$("$CLI" --session qa qa report)"
+echo "$BLOCKED_DOWNLOAD_REPORT" | grep -q '"kind":"download-blocked"'
+echo "$BLOCKED_DOWNLOAD_REPORT" | grep -q '/download.txt'
+"$CLI" --session qa qa clear | grep -q '"cleared"'
STEP="artifacts-visual"
"$CLI" --session qa screenshot --output viewport.png | grep -q '"name":"viewport.png"'
"$CLI" --session qa screenshot --full-page --output full-page.png | grep -q '"name":"full-page.png"'
diff --git a/docs/roadmap/improvements-backlog.md b/docs/roadmap/improvements-backlog.md
index f5e1697..7dfb897 100644
--- a/docs/roadmap/improvements-backlog.md
+++ b/docs/roadmap/improvements-backlog.md
@@ -287,7 +287,10 @@ screenshot-plan caps and 96 px deduplication, budget text fallback, and stale
element-reference errors. Recording coverage now locks every format/quality
argument mapping, strict executable discovery, consecutive-failure aborts, and
bounded stop timeouts; capabilities and oversized socket requests are covered.
-Peer-credential and host navigation/download classification gaps remain.
+Linux CI also connects through a deliberately different uid and requires
+`PEER_DENIED`; Linux and macOS E2E assert `navigation-blocked` and
+`download-blocked` diagnostics respectively. **Done:** every gap named in D2
+now has direct regression coverage.
**D3. Web CI:** ~~`next build` + eslint on PR (site can break invisibly today).~~
**Done** — the `web` job in `ci.yml` runs `pnpm --filter @headless/web lint`
From b3a50fe3867b7a03225071438a90ccba5dc0b339 Mon Sep 17 00:00:00 2001
From: Aditya Garud <153842990+yashranaway@users.noreply.github.com>
Date: Mon, 10 Aug 2026 19:00:05 +0000
Subject: [PATCH 2/4] test(e2e): isolate host-blocked navigation fixture
---
apps/headless/Tests/Fixtures/dashboard.html | 3 ++-
apps/headless/Tests/linux-e2e.sh | 2 +-
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/apps/headless/Tests/Fixtures/dashboard.html b/apps/headless/Tests/Fixtures/dashboard.html
index 6b0977e..a6628ec 100644
--- a/apps/headless/Tests/Fixtures/dashboard.html
+++ b/apps/headless/Tests/Fixtures/dashboard.html
@@ -31,7 +31,8 @@ Continue setup
Credential-bearing URL
Suspicious installer
Download fixture
-
+
+
diff --git a/apps/headless/Tests/linux-e2e.sh b/apps/headless/Tests/linux-e2e.sh
index f0cf8ae..dd1599d 100755
--- a/apps/headless/Tests/linux-e2e.sh
+++ b/apps/headless/Tests/linux-e2e.sh
@@ -128,7 +128,7 @@ if SUSPICIOUS_RESULT="$(headless --session qa click --role link --name 'Suspicio
exit 1
fi
echo "$SUSPICIOUS_RESULT" | grep -q 'UNSAFE_RESOURCE_TYPE'
-headless --session qa click --role button --name 'Scripted non-web navigation' | grep -q '"clicked"'
+headless --session qa click --role button --name 'Scripted host-blocked navigation' | grep -q '"clicked"'
headless --session qa wait --url /designers/dashboard --settled --timeout 10000 | grep -q 'Designers Dashboard'
BLOCKED_NAVIGATION_REPORT="$(headless --session qa qa report)"
echo "$BLOCKED_NAVIGATION_REPORT" | grep -q '"kind":"navigation-blocked"'
From 24fb13ecf6818fb48848ed7ffcf45483de73b018 Mon Sep 17 00:00:00 2001
From: Aditya Garud <153842990+yashranaway@users.noreply.github.com>
Date: Mon, 10 Aug 2026 19:02:59 +0000
Subject: [PATCH 3/4] test(macos): identify navigation denial failures
---
apps/headless/Tests/macos-e2e.sh | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/apps/headless/Tests/macos-e2e.sh b/apps/headless/Tests/macos-e2e.sh
index 4362b47..499d419 100755
--- a/apps/headless/Tests/macos-e2e.sh
+++ b/apps/headless/Tests/macos-e2e.sh
@@ -185,31 +185,38 @@ echo "$QA_REPORT" | grep -q '"kind":"local-not-found"'
STEP="safe-input-navigation"
"$CLI" --session qa fill @e1 'Ada Lovelace' | grep -q '"valueLength":12'
"$CLI" --session qa press Escape | grep -q '"pressed":"Escape"'
+STEP="safe-input-external-link"
if EXTERNAL_RESULT="$("$CLI" --session qa click --role link --name 'External application')"; then
echo "external application link was not blocked" >&2
fail
fi
echo "$EXTERNAL_RESULT" | grep -q 'UNSAFE_NAVIGATION'
+STEP="safe-input-non-web-link"
if NON_WEB_RESULT="$("$CLI" --session qa click --role link --name 'Non-web browser URL')"; then
echo "non-HTTP browser URL was not blocked" >&2
fail
fi
echo "$NON_WEB_RESULT" | grep -q 'UNSAFE_NAVIGATION'
+STEP="safe-input-credential-link"
if CREDENTIAL_RESULT="$("$CLI" --session qa click --role link --name 'Credential-bearing URL')"; then
echo "credential-bearing browser URL was not blocked" >&2
fail
fi
echo "$CREDENTIAL_RESULT" | grep -q 'UNSAFE_NAVIGATION'
+STEP="safe-input-suspicious-link"
if SUSPICIOUS_RESULT="$("$CLI" --session qa click --role link --name 'Suspicious installer')"; then
echo "suspicious installer link was not blocked" >&2
fail
fi
echo "$SUSPICIOUS_RESULT" | grep -q 'UNSAFE_RESOURCE_TYPE'
+STEP="safe-input-scripted-navigation"
"$CLI" --session qa click --role button --name 'Scripted non-web navigation' | grep -q '"clicked"'
"$CLI" --session qa wait --url /designers/dashboard --settled --timeout 10000 | grep -q 'Designers Dashboard'
+STEP="safe-input-download"
"$CLI" --session qa click --role link --name 'Download fixture' | grep -q '"clicked"'
sleep 1
BLOCKED_DOWNLOAD_REPORT="$("$CLI" --session qa qa report)"
+STEP="safe-input-download-report"
echo "$BLOCKED_DOWNLOAD_REPORT" | grep -q '"kind":"download-blocked"'
echo "$BLOCKED_DOWNLOAD_REPORT" | grep -q '/download.txt'
"$CLI" --session qa qa clear | grep -q '"cleared"'
From 8ebfa91ff3a98914a63c929e9e2300f95f3e3fb4 Mon Sep 17 00:00:00 2001
From: Aditya Garud <153842990+yashranaway@users.noreply.github.com>
Date: Mon, 10 Aug 2026 19:05:52 +0000
Subject: [PATCH 4/4] fix(macos): classify and cancel download intent
---
CHANGELOG.md | 3 +++
apps/headless/main.swift | 17 +++++++++++++++--
2 files changed, 18 insertions(+), 2 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f93105b..767c097 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -38,6 +38,9 @@ Cutting that release is tracked in
accuracy, and oversized Unix-socket requests.
- Cross-uid Unix-socket rejection and host E2E classification checks for
blocked top-frame navigation and denied page-initiated downloads.
+- macOS explicitly converts download-intent links, attachment responses, and
+ unsupported response types into cancellable `WKDownload` objects so every
+ denied download is classified without writing page-controlled bytes.
- Progressive context pruning: `inspect --context summary|outline|text|actions|full`
with `--task` ranking, `--within @rN` scoping, and `--limit` / `--budget` /
`--depth` bounds. Every focused response reports `contextStats` and `omitted`.
diff --git a/apps/headless/main.swift b/apps/headless/main.swift
index 50a98d4..57962b4 100644
--- a/apps/headless/main.swift
+++ b/apps/headless/main.swift
@@ -706,6 +706,12 @@ final class BrowserWindowController: NSWindowController, NSWindowDelegate,
decisionHandler(.cancel)
return
}
+ if navigationAction.shouldPerformDownload {
+ // Convert explicit download intent into WKDownload so the
+ // delegate below can classify and cancel it deterministically.
+ decisionHandler(.download)
+ return
+ }
// Hand non-web schemes (mailto:, facetime:, app links…) to the system.
if let url = navigationAction.request.url, let scheme = url.scheme?.lowercased(),
!["http", "https", "file", "about", "data", "blob", "javascript"].contains(scheme) {
@@ -721,10 +727,17 @@ final class BrowserWindowController: NSWindowController, NSWindowDelegate,
if let response = navigationResponse.response as? HTTPURLResponse {
qaBridge.store.append(kind: "response", url: response.url?.absoluteString,
method: "GET", status: Double(response.statusCode))
+ if response.value(forHTTPHeaderField: "Content-Disposition")?
+ .lowercased().contains("attachment") == true {
+ decisionHandler(.download)
+ return
+ }
}
if !navigationResponse.canShowMIMEType {
- showToast("Can’t display this file type")
- decisionHandler(.cancel)
+ // Unsupported navigation responses would otherwise become
+ // implicit downloads. Route them through the same fail-closed
+ // cancellation and diagnostic path.
+ decisionHandler(.download)
return
}
decisionHandler(.allow)