0.7.0: CDP/debugger-layer enforcement of blocked domains (validated) + hardening - #66
Conversation
The sites step hardcoded action:'allow', so no rule a user could create ever carried a block pattern — and the CDP enforcement layer (ruleHasBlockPattern gate) could never arm. Adds an Allow/Block selector to the sites step, shows each pattern's action on the confirm step, and removes the wizard's innerHTML sinks (same no-sink rule popup.ts is held to, now locked in for wizard.ts too).
Receipt-log ground truth on a two-hostname fixture server; egress battery through every vector the page realm never wrapped (WebSocket, EventSource, Image.src, Worker fetch, iframe fetch). Phases: default-off (doubles as the battery's vacuousness control), enforcement via the real popup wizard + settings toggle, per-tab scope, navigation block, setting-off/re-enable, tab close, delegation expiry across restart, restart self-heal, kill switch. SKIP-ENV on a focusless desktop, never a false pass.
…koff
Review follow-ups from the 0.6.0 disposition, batched for 0.7.0:
- On-disk cache format is versioned ({v, entries}); a record written by a
different format (incl. the 0.6.x flat shape) is dropped and refetched,
never half-parsed. The opt-out deleter still counts legacy bytes.
- Consecutive unreachable settles double the negative TTL (5m..4h cap),
so a persistently broken origin is contacted at most ~6x/day instead of
~288x — each request being a fingerprintable third-party signal (ADR-009).
Any ok/none settle resets the counter.
- Negative URL-parse lock-ins (backslash authority-cut, %00 hostname,
traversal, scheme case, embedded newlines) and a concurrent clear/write
chaos battery over every reachable enqueue interleaving.
When a detect-only external driver is shown, the card now states the browser's own posture: Chrome 136+ already refuses remote debugging of the default profile (so an agent that reached the tab is running in a separate profile or was granted debugging at launch); older Chrome gets the concrete fix (update) instead of only the exposure. Version-derived only — the extension cannot see which profile it runs in, so the line never claims the current profile or this agent is contained.
…/008) - architecture §7/§8: CDP layer is shipped in 0.7.0, enforces site block patterns for network egress incl. the unwrapped vectors, opt-in/default off/fail-safe; correct the one-client-per-tab premise (multi-client Chrome: our attach usually succeeds alongside an external driver, a strengthening we do NOT rely on vs a browser-level adversary). - ADR-007 Correction 2 + ADR-008 R1 marked shipped (version half). - README + both store surfaces: Browser-layer blocking setting, blocked sites in Limited, debugger permission justification covers CDP attach. - enforceability.ts: per-action limit is attribution, not the debugger slot. Disclosure-consistency test green (17).
CHANGELOG folded before the bump (self-revert trap); manifest/package/ README/dist all at 0.7.0. Tag is held until the full gate — unit suite + whole smoke battery incl smoke:cdp + HMA secure — is green on this exact artifact.
…a debugger Live validation (smoke:cdp + a focus-free mechanism harness) showed the CDP Fetch domain does not pause ws:// handshakes, and Network.setBlockedURLs blocking of WebSocket raced the handshake and leaked intermittently on the live extension's shared debugger session. Rather than ship a flaky block behind a 'closed' claim, this layer does not attempt WebSocket blocking. - cdp-enforcement.ts: reverted the Network.setBlockedURLs approach; added a scope note. The 6 vectors it DOES close (fetch/XHR, EventSource, element-src, Worker/iframe fetch, navigation) are mech-proven on the real extension path. - smoke:cdp: CLOSED_VECTORS must block; ws asserted to STAY OPEN under enforcement, so the scope line is checked, not assumed. - architecture §7/§8, ADR-007 (Correction 3), ADR-008 R2 (now the home for deterministic ws blocking via declarativeNetRequest), CHANGELOG, README, both store surfaces: corrected to disclose WebSocket as the one open vector. Disclosure-consistency green.
…EEN (26 checks) Restructured for headful reliability after live runs exposed harness (not product) issues: - Resilient popup acquisition: poll target.page()/asPage() past 'Requesting main frame too early', and re-establish an active Chrome window (OS-activate + windows.update focused + bring a real tab front) before every openPopup — page churn kept taking OS focus away mid-run. - Two popup interactions only (wizard + enable), both early where focus is fresh; all teardown phases (tab-close, kill, expiry, self-heal) driven by storage + restart, which is deterministic and needs no popup. - Block-reporting asserted via lifetimeStats.totalActionsBlocked (persisted), not the sessions timeline (only flushed at session end). - Re-activate the rule explicitly in the self-heal phase: expiry and the kill both persist isActive:false (index.ts:1481/1213), so a timeBound-only refresh left the rule inactive and nothing attached. - Watchdog 360s -> 900s; protocolTimeout 30s -> 45s. Gate result on the built dist/: 26/26 — default-off attaches nothing; the real popup wizard authors the block rule and the real toggle arms it; every CDP-closable vector (EventSource, Image.src, Worker/iframe fetch) + navigation is blocked to blocked.test while WebSocket stays open (asserted); blocks reported; per-tab scope holds; and enforcement tears down on tab-close, kill latch, and expiry, and self-heals on restart.
… HIGH) url/match-pattern.ts's contract requires callers to validate block patterns at input time — the matcher fail-CLOSES on a non-match, so for a BLOCK a non-match is fail-OPEN (the request the user meant to block sails through). The wizard is the first and only author of block patterns and had no validation, so ordinary inputs silently blocked nothing while the UI said 'blocked': - https://evil.com (scheme, no path) matched no real request path - evil.com matched only the apex, not www.evil.com - 5+ wildcards / lone * / ** were inert or bricked the tab normalizeBlockPattern reduces any URL to its host, strips a bare :port, rejects empty/path/whitespace/lone-star/over-broad/non-host inputs with a plain-language reason, and expands a plain host to apex + **.subdomains. Round-trip tests against the real matcher prove the stored patterns block the request (red on the pre-fix verbatim-store path, confirmed: matchUrlPattern('https://evil.com/steal', 'https://evil.com') === false). Also marked the three stale 'closes WebSocket' lines in ADR-007 body inline as superseded by Correction 3. Found by the Phase 4.5 adversarial self-review.
…rial pass) A focused adversarial re-review of the first block-validation fix found the same fail-open class in adjacent inputs, plus a cross-layer disagreement: - Case/dot canonicalization: the matcher lowercases + trailing-dot-strips the request HOST but never the pattern, so 'Evil.Com', 'evil.com.', '.evil.com' were stored verbatim and matched nothing (silent fail-open). Now lowercased + dot-trimmed up front. - Over-broad wildcards: '*.com'/'**.com'/'**.*'/'*.*.*.*' passed the lone-star reject and each blocked a public-suffix-wide swath (bricking the tab). Now the registrable tail (last two labels) must be literal. - Backslash authority: 'https://evil.com\@x.com' resolves to host evil.com in the browser but the '/'-only split yielded x.com — wrong host blocked. Now '\' ends the authority like the browser folds it. - Malformed labels ('a..b.com', leading/trailing hyphen) validated per DNS grammar. - CROSS-LAYER: decideFetchRequest (CDP) is block-any; evaluateSitePatterns (page-realm monitor/downloads) was first-match-wins, so 'allow *.example.com' before 'block ads.example.com' made the page realm PERMIT what the CDP layer blocked. A block now wins over allow regardless of order, in both layers. Round-trip tests against the real matcher (red on the pre-canon store) and a block-precedence regression (red on first-match-wins) added. 1043 tests green.
There was a problem hiding this comment.
Security Review: AI Browser Guard v0.7.0 - CDP Enforcement Layer
Executive Summary
This PR introduces browser-layer (CDP/debugger) enforcement of blocked domains, an opt-in feature that mediates network requests below the page realm where in-page tampering cannot reach. The implementation is security-conscious with strong fail-safe defaults, but has critical input validation gaps and permission/capability boundary issues that must be addressed.
CRITICAL Findings
1. HTML Injection in Wizard Confirm Step ⚠️ CRITICAL
File: src/delegation/wizard.ts (inferred from test coverage)
Issue: User-typed site patterns are rendered in the confirm step. While the DOM test asserts they render as text, the actual rendering code is not in the diff. If renderWizard uses innerHTML or similar for pattern display, hostile input like <img src=x onerror=alert(1)> could execute.
Evidence from test:
// wizard.dom.test.ts:113
const hostile = '<img src=x onerror="window.__pwned=1">';
addPattern(container, hostile, 'allow');
// ... advances to confirm step
expect(container.textContent).toContain(hostile);
expect(container.querySelector('img')).toBeNull();Attack Vector:
- User types
<script>steal(chrome.storage.local)</script>as a site pattern - Wizard stores it (allow patterns are stored as-typed per test comment)
- Confirm step renders it via
innerHTML→ XSS in extension context - Attacker reads delegations, sessions, ai-safety cache
Mitigation Required:
- VERIFY
renderWizardusestextContentor DOM APIs for pattern display, neverinnerHTML - Add explicit test: pattern with
<script>,<img onerror>,javascript:renders inertly - Consider: sanitize patterns at storage time (reject
<,>,",'outright)
2. Backslash Authority Bypass in URL Parsing ⚠️ CRITICAL
File: src/aisafety/client.ts
Issue: The URL parser test reveals a critical assumption: \\ in https:// URLs acts as a path separator, so https://good.example\\@evil.example/ parses as host good.example. BUT the test does not prove the fetch target is also good.example — if the code constructs the fetch URL via string concat instead of using the parsed origin, the backslash bypass survives.
Test shows awareness, not proof:
// client.test.ts:492
expect(declarationOriginFor('https://good.example\\@evil.example/')).toBe('https://good.example');Attack Vector (if vulnerable):
// Hostile page supplies this as its URL
lookupAiSafetyDeclaration('https://good.example\\@evil.example/.well-known/ai-safety.txt');
// If fetch is built as: fetch(pageUrl + '/.well-known/ai-safety.txt')
// Result: fetch('https://good.example\\@evil.example/.well-known/ai-safety.txt')
// Which the *browser* resolves to evil.example, not good.exampleMitigation Required:
-
VERIFY fetch URL is constructed as
new URL('/.well-known/ai-safety.txt', origin).hrefwhereoriginis the WHATWG-parsed origin, never string concatenation - The current code shows
fetch(\${origin}${WELL_KNOWN_PATH}`)` which IS string concat — MUST be replaced with URL constructor - Add test: supply
https://good.example\\@evil.example/and mock fetch, assert fetch was called withhttps://good.example/.well-known/ai-safety.txt(not evil.example)
Current Code (client.ts:308):
response = await fetch(`${origin}${WELL_KNOWN_PATH}`, { ... });☠️ This is direct concatenation. If origin is https://good.example\\@evil.example, the fetch target is that literal string, which Chrome resolves to evil.example.
3. Race Condition in CDP Detach 🔴 HIGH
File: src/background/cdp-enforcement.ts (inferred)
Issue: The diff describes detach on "delegation expiry, kill switch, tab close, setting-off" but does not show the detach sequencing. If detach is async and another attach can start before the prior detach completes, two debugger sessions could collide, or a detach could remove a re-attached session.
Attack Scenario:
- User toggles setting OFF → detach starts (async)
- Before detach completes, user toggles ON → attach starts
- Both operations run concurrently on the same tabId
- Result:
chrome.debugger.detach(tabId)removes the NEW session, enforcement dies silently
Mitigation Required:
- Serialize attach/detach per tabId with a per-tab lock or operation queue
- Test: rapid ON-OFF-ON-OFF cycles must end with correct final state (ON = attached, OFF = detached)
- Ensure detach is idempotent (detaching an unattached tab is no-op, not error)
HIGH Findings
4. Block Pattern Validation Gaps 🔴 HIGH
File: src/delegation/wizard.ts (inferred)
Issue: normalizeBlockPattern canonicalizes case and dots, but the diff does not show validation for:
- Overlength hosts (DNS labels max 63 bytes, FQDN max 253) → DoS via storage bloat
- Invalid characters (space,
<,>, unicode spoofs) → silent match failure - Empty after trim → stored as
'', matches nothing - IPv6 literals → matcher may not handle
[2001:db8::1]correctly
Current Test Coverage (wizard.dom.test.ts):
expect(normalizeBlockPattern('Evil.Com')).toEqual(...); // case
expect(normalizeBlockPattern('evil.com.')).toEqual(...); // trailing dotMissing: empty, overlength, invalid chars, IPv6, unicode homographs
Mitigation:
- Add to
normalizeBlockPattern:if (host.length === 0 || host.length > 253) return { error: 'Invalid hostname length' }; if (!/^[a-z0-9.-]+$/.test(host)) return { error: 'Invalid characters in hostname' };
- Reject patterns that reduce to empty after canonicalization
- Test: all rejection cases return
{ error: '...' }and UI shows the error
5. WebSocket Scope Drift Risk 🔴 HIGH
File: scripts/smoke-cdp-enforcement.mjs, src/background/cdp-enforcement.ts
Issue: The smoke test asserts WebSocket is NOT blocked (the documented scope boundary), but this is a runtime check, not a compile-time contract. If a future CDP change makes Fetch.requestPaused start emitting for ws://, the code would silently start blocking WebSocket without updating the documentation or the user-facing "WebSocket still connects" disclaimer.
Risk:
- User blocks
evil.comexpecting only HTTP egress to be stopped - Chrome update makes CDP block WebSocket too
- Extension now blocks WebSocket, violating the documented scope
- User loses WebSocket connectivity to a blocked domain (possibly their own allowlisted-then-blocked site) with no warning
Mitigation:
- Feature flag:
const CDP_BLOCKS_WEBSOCKET = false;incdp-enforcement.ts - Smoke test asserts
CDP_BLOCKS_WEBSOCKET === falseand WebSocket passes - If a future Chrome makes ws pauseable, the test fails LOUDLY, forcing an update to docs + flag
- Never silently transition from "ws open" to "ws blocked"
6. Kill Switch Tab Close Race 🔴 HIGH
File: Not shown in diff, inferred from CHANGELOG
Issue: Kill switch "closes the tabs an agent controls" (README, store listing). If close is via chrome.tabs.remove(tabId), a race exists:
- Kill switch fires →
tabs.remove(agentTabId) - Agent's external driver reopens tab before
tabs.removecallback fires - New tab has same URL, agent re-attaches
- User thinks they killed it, agent is still running
Current Test (smoke-arming.mjs, not in diff):
Likely tests that tab closes, not that agent cannot immediately return.
Mitigation:
- After
tabs.remove, blacklist the closed tab's origin for 30s (per-origin cooldown) - If agent is detected on that origin within cooldown, auto-kill again and show notification
- Test: rapid kill → agent reopens → auto-kill fires → user sees escalation warning
MEDIUM Findings
7. Cache Format Version Downgrade Attack 🟡 MEDIUM
File: src/aisafety/cache.ts
Issue: Cache is versioned (CACHE_FORMAT_VERSION = 2), and a mismatched version is dropped. But there is no upgrade-only enforcement — an attacker who can write to chrome.storage.local could downgrade to version 1 (pre-backoff), forcing the extension to refetch on every lookup and fingerprint the user to ai-safety.txt origins.
Attack:
// Malicious extension or content script with storage access
chrome.storage.local.set({
aiSafetyDeclarationCache: { v: 1, entries: {} }
});
// ABG reads v=1, drops it, refetches on every agent detectionMitigation:
- Store
minSupportedVersionin settings:{ minSupportedVersion: 2 } - On cache read, if
cache.v < minSupportedVersion, drop it and refuse to write v < minSupportedVersion - An attacker can still clear the cache, but cannot force an older format
8. Exponential Backoff Integer Overflow 🟡 MEDIUM
File: src/aisafety/client.ts
Issue: Backoff uses 2 ** priorFailures, capped at MAX_BACKOFF_DOUBLINGS = 10. But if priorFailures is corrupted (storage poisoning) and exceeds 31, 2 ** 32 is 4294967296 (4.2 billion ms = 49 days), and if it reaches 53 (float precision limit), the TTL becomes Infinity.
Current Code (client.ts:285):
const ttlMs = Math.min(
UNREACHABLE_CACHE_TTL_MS * 2 ** Math.min(priorFailures, MAX_BACKOFF_DOUBLINGS),
UNREACHABLE_BACKOFF_CAP_MS,
);Safe because of Math.min(priorFailures, MAX_BACKOFF_DOUBLINGS), BUT getUnreachableFailureCount does not validate the stored failures field — if it is Infinity or NaN, the result propagates.
Mitigation:
- In
getUnreachableFailureCount:const failures = entry.failures ?? 1; if (!Number.isFinite(failures) || failures < 0) return 0; return Math.floor(Math.min(failures, MAX_BACKOFF_DOUBLINGS));
9. Settings UI Race in Smoke Test 🟡 MEDIUM
File: scripts/smoke-cdp-enforcement.mjs
Issue: The smoke test drives the settings toggle via ensurePopup() → DOM click, but the comment admits openPopup gets flaky "deep into a run" and "hangs, not fails". If the hang is a deadlock (not a timeout), the test never exits, CI hangs forever.
Evidence:
// smoke-cdp-enforcement.mjs:163
// The "attach fails -> fall back to page realm" path ... is unit-covered, but
// DevTools-open is no longer a reliable way to produce one.This implies DevTools-open no longer blocks attach (multi-client), so the fallback path is untested in the smoke suite.
Mitigation:
- Add timeout to
ensurePopup(): if popup does not open in 10s, fail the test (not hang) - Test fallback path: mock
chrome.debugger.attachto reject, assert enforcement is OFF and page-realm deny still works
10. Unbounded Receipt Log in Smoke Test 🟡 MEDIUM
File: scripts/smoke-cdp-enforcement.mjs
Issue: The receipts array is never cleared between phases, and each phase fires 6+ vectors. If the test runs 10 phases, receipts holds 60+ entries, all kept in memory for the test's 900s watchdog. Not a production issue, but a test reliability issue — if the fixture server leaks receipts or Chrome caches responses, phase assertions could see stale data.
Example:
// smoke-cdp-enforcement.mjs:65
receipts.push(`${req.headers.host}${req.url}`);
// Never cleared; receiptsFor() searches the entire arrayMitigation:
- Add
receipts.length = 0;at the start of each phase - Or:
const receipts = { phaseA: [], phaseB: [], ... }and search per-phase
TypeScript Correctness
11. Untyped normalizeBlockPattern Return 🟡 MEDIUM
File: src/delegation/wizard.ts (inferred)
The test expects { patterns: [...], error?: string }, but if the function returns { patterns: [] } for an invalid input, the caller cannot distinguish "zero patterns (valid empty)" from "rejected". Type the return:
type NormalizeResult =
| { patterns: SitePattern[] }
| { error: string };
function normalizeBlockPattern(input: string): NormalizeResult {
// ...
if (invalid) return { error: 'reason' };
return { patterns: [...] };
}Then callers must if ('error' in result) before accessing patterns.
Test Coverage Gaps
12. CDP Enforcement Unit Tests Missing 🟡 MEDIUM
The diff adds src/background/cdp-enforcement.ts but no test file cdp-enforcement.test.ts. The smoke suite exercises it end-to-end, but unit tests are absent for:
ruleHasBlockPatternedge cases (empty rule, null scope, patterns without action)decideFetchRequestwith malformed CDP events- Attach/detach idempotency (detach an unattached tab, attach an already-attached tab)
- Concurrent attach/detach on the same tabId
Required:
src/background/cdp-enforcement.test.tscovering all exported functions- Mock
chrome.debugger.attach/detachto test failure paths
13. Wizard DOM Tests Are Incomplete 🟡 MEDIUM
wizard.dom.test.ts tests the happy path (add allow, add block, advance to confirm), but missing:
- Duplicate pattern (user types
evil.comtwice → UI shows error or dedupes silently?) - Invalid pattern (user types
*with trailing space → rejected or trimmed?) - Remove pattern (UI has delete buttons per the real wizard, not tested)
- Back navigation (confirm → back to sites → patterns still there?)
Performance
14. Cache Eviction is O(n²) in Entry Count 🟡 MEDIUM
File: src/aisafety/cache.ts
evict() iterates all entries to find expired ones (O(n)), then sorts by seq (O(n log n)), then slices (O(n)). This is called on every write. With MAX_CACHE_ENTRIES = 50, it is cheap, but the comment says "generous relative to real use" — if a future change raises the cap to
…a fixed tick CI Test failed intermittently on 'wraps fetch under an active rule' — the MessagePort rule-update delivery in jsdom is a task whose timing isn't guaranteed within one macrotask, so under CI load the assertion read the arming state one step early (fetch still native). Passes locally every run; surfaced in CI when this PR's added tests shifted the schedule. setRule/ setKillSwitch now poll for the expected wrapped/unwrapped state (up to 1s) instead of a single setTimeout(0). Test-only; no shipped code changes.
There was a problem hiding this comment.
Security Review: AI Browser Guard PR 0.7.0
Executive Summary
This PR implements browser-layer (CDP/debugger) enforcement of blocked domains plus hardening improvements. The implementation is security-conscious with proper fail-safe defaults, but there are CRITICAL findings around input validation and HIGH findings around permission scope that require changes before merge.
CRITICAL Findings
1. Incomplete URL Validation in Block Pattern Normalization
Location: src/delegation/wizard.ts (inferred from tests, actual implementation not shown in diff)
Issue: While tests verify canonicalization, the diff doesn't show the normalizeBlockPattern implementation. The tests reference it but the actual validation code is missing from the diff.
Evidence from tests:
// From wizard.dom.test.ts
import { normalizeBlockPattern } from './wizard';Risk: If normalizeBlockPattern doesn't exist or has incomplete validation, malicious patterns could bypass blocking or cause injection issues.
Required: Show the complete implementation of normalizeBlockPattern and its validation logic.
2. Potential XSS in Pattern Display
Location: src/delegation/wizard.dom.test.ts:95-98
const text = container.textContent ?? '';
expect(text).toContain('allowed.test (allow)');Issue: The test uses textContent (safe) but the actual rendering code isn't shown. If the wizard renders user-typed patterns via innerHTML or DOM manipulation without sanitization, it's vulnerable.
Required: Verify the actual renderWizard implementation escapes user input when displaying patterns in the UI.
HIGH Findings
1. Debugger Permission Scope Expansion Without User Notice
Location: docs/chrome-web-store-description.md:105-106
debugger:
Detects when an automation framework... and, when the user enables the optional Browser-layer blocking setting, attaches a CDP session to tabsIssue: The debugger permission was previously read-only (detection). Now it's used for active enforcement (attach/block requests). While opt-in, the permission description change may not trigger a re-review or user re-consent flow.
Chrome Web Store Policy: Permission scope changes can require re-justification even if the permission was already declared.
Mitigation: Ensure the CWS listing update clearly highlights this scope change. Consider a runtime consent dialog on first enable of the CDP enforcement setting.
2. CDP Enforcement Fail-Open Without Audit Trail
Location: src/background/cdp-enforcement.ts:79 (referenced in docs)
From architecture.md:
On attach failure it **fails safe to the page-realm interceptor**Issue: While fail-open is the correct safety design, attachment failures aren't logged or reported to the user. A persistent attach failure means enforcement silently degrades without user awareness.
Scenario:
- User enables browser-layer blocking expecting CDP enforcement
- Chrome is in a state where attach always fails (e.g., managed policy, unusual extension conflict)
- User believes they have browser-layer protection but only have page-realm (re-patchable) protection
Required: Add telemetry (non-PII) or UI indication when CDP enforcement cannot attach, so users know their actual protection level.
3. Cache Format Version Handling Lacks Migration Path
Location: src/aisafety/cache.ts:164-174
const wrapper = raw as Record<string, unknown>;
if (wrapper.v !== CACHE_FORMAT_VERSION) return {};Issue: Format version mismatches silently drop all cached entries. While acceptable for cache invalidation, there's no upgrade path if future versions need to preserve existing data.
Risk: A buggy version bump could cause excessive refetch traffic during rollout (though capped by backoff, per ADR-009).
Recommendation: Document the "drop everything on version mismatch" policy as intentional in the code comment (it may be fine, just needs explicit acknowledgment).
MEDIUM Findings
1. WebSocket Scope Gap Disclosure Timing
Location: Multiple (architecture.md, ADR-007, store listing)
Issue: WebSocket being the one undocumented vector is now prominently disclosed, but users who enabled the feature before this version won't see the updated description unless they revisit settings.
Recommendation: Add an in-extension notice (one-time popup or badge) for existing users who have CDP enforcement enabled, explaining the WebSocket scope limitation.
2. Unreachable Backoff Counter Overflow
Location: src/aisafety/cache.ts:344-356
const MAX_BACKOFF_DOUBLINGS = 10;
// ...
UNREACHABLE_CACHE_TTL_MS * 2 ** Math.min(priorFailures, MAX_BACKOFF_DOUBLINGS)Issue: priorFailures is user-controlled storage. If corrupted or manipulated to a large number, 2 ** Math.min(priorFailures, 10) is safe (max 1024x), but there's no validation that priorFailures is non-negative.
Scenario:
failures: -1 → 2 ** -1 = 0.5 → TTL becomes 2.5 minutes (shorter than intended)
failures: NaN → 2 ** NaN = NaN → TTL becomes NaN → cache breaksFix:
const failures = Math.max(0, Math.floor(priorFailures)) || 0;
const ttlMs = Math.min(
UNREACHABLE_CACHE_TTL_MS * 2 ** Math.min(failures, MAX_BACKOFF_DOUBLINGS),
UNREACHABLE_BACKOFF_CAP_MS,
);3. CDP Enforcement Relies on Multi-Client Chrome Behavior
Location: docs/adr/007-cdp-layer-enforcement.md:73-86 (Correction 2)
The one-client-per-tab premise above is stale on current Chrome: multi-client DevTools protocol support means chrome.debugger.attach() **succeeds** alongside an external CDP clientIssue: This is a strengthening (good), but it's also a Chrome implementation detail not guaranteed by API contract. A future Chrome change could revert to one-client-per-tab.
Risk: Over-claiming protection against external drivers based on current Chrome behavior could become an overclaim if Chrome regresses.
Mitigation: The documentation correctly hedges ("typically succeeds", "does not rely on"), but ensure no marketing copy or UI text makes an unqualified claim like "blocks external agents."
LOW Findings / Observations
1. Test Coverage: CDP Enforcement Is Well-Gated
Positive: npm run smoke:cdp is a real-browser validation against receipts. This is the right engineering for a security-critical feature.
Observation: The smoke test runs headful and requires OS focus, which could break in CI. Ensure CI environment is configured to provide a display or skip gracefully (exit 3 is correct).
2. Manifest Version Bump
Location: manifest.json:4
"version": "0.7.0",Observation: Version properly bumped. No security issue, but confirm Web Store submission is prepared for the debugger permission scope clarification.
Chrome Extension Best Practices
✅ PASS: MV3 Compliance
- Content script isolation maintained (ISOLATED vs MAIN realms)
- Message passing via
chrome.runtimeports - No
eval()or unsafe code generation
✅ PASS: CDP Enforcement Design
- Opt-in by default (no runtime banner without user action)
- Per-tab scoping (non-delegated tabs untouched)
- Fail-open on attach failure (never blocks user's own browsing)
⚠️ CAUTION: Debugger Permission UX
- Chrome shows "started debugging this browser" bar while attached (correct, but potentially alarming to users)
- Store listing now describes this, but consider in-extension onboarding flow for first enable
TypeScript Correctness
✅ PASS: Strict Mode Compliance
- Tests import types correctly (
import type { ... }) - No
anytypes introduced in diff - Proper async/await handling in cache lock
⚠️ MINOR: Missing Type Guards in Cache
Location: src/aisafety/cache.ts:151
if (entry.failures !== undefined
&& (typeof entry.failures !== 'number' || !Number.isFinite(entry.failures))) return false;Observation: This validates the shape but doesn't validate the value (e.g., failures: -1 passes). Consider adding:
if (entry.failures !== undefined &&
(typeof entry.failures !== 'number' || !Number.isFinite(entry.failures) || entry.failures < 0))
return false;Test Coverage Analysis
✅ EXCELLENT: 1032 Tests (up from 990)
- Cache format versioning: ✅ Comprehensive
- Unreachable backoff: ✅ Covered (but see MEDIUM #2 re: negative values)
- CDP enforcement: ✅ Real-browser smoke test (headful Chrome)
- Block pattern precedence: ✅ Unit + DOM tests
❌ MISSING:
- Negative test for
normalizeBlockPatternwith injection attempts (e.g.,<script>,javascript:, null bytes) - CDP attach failure path (unit test exists per docs, but not shown in diff - verify it exists)
- Concurrent CDP operations (what if two tabs try to attach simultaneously?)
Security-Critical Paths Requiring Validation
1. Block Pattern Input Validation (CRITICAL)
Path: User input → normalizeBlockPattern → storage → CDP Fetch.failRequest
Required Tests:
// Must reject or sanitize:
normalizeBlockPattern('javascript:alert(1)')
normalizeBlockPattern('<script>alert(1)</script>')
normalizeBlockPattern('evil.com\x00.attacker.com')
normalizeBlockPattern('*.com') // overly broadStatus: Tests reference this function but implementation not in diff. BLOCK MERGE until validation logic is shown.
2. CDP Request Decision Path (HIGH)
Path: Fetch.requestPaused → decideFetchRequest → Fetch.failRequest / continueRequest
From docs:
"a fail-open decision path means a policy bug can never hang a paused request"
Required: Verify decideFetchRequest has:
- Timeout on decision logic (don't hang the browser if rule evaluation deadlocks)
- Explicit
try/catchwith fail-open default - Logging of decision errors
Status: Implementation not in diff. Request code review of src/background/cdp-enforcement.ts.
Performance Considerations
✅ PASS: Content Script Overhead
- Network wrappers installed only while armed (0.6.1 improvement retained)
- CDP enforcement is per-tab and detaches on delegation end
✅ PASS: Memory Leaks
- Cache eviction at 50 entries (MAX_CACHE_ENTRIES)
- Backoff prevents runaway refetch on broken origins
⚠️ WATCH: CDP Event Volume
From architecture.md:
"the CDP Fetch domain pauses EVERY request on the tab"
Risk: On a tab with heavy ad network traffic or websockets (despite ws not being paused), the event volume to the service worker could be high.
Recommendation: Add rate-limiting or circuit breaker if Fetch.requestPaused events exceed a threshold (e.g., 100/sec).
Recommended Changes
MUST FIX (Block Merge):
- Show
normalizeBlockPatternimplementation with full validation - Verify
renderWizardescapes user input when displaying patterns - Add negative value validation to cache
failurescounter
SHOULD FIX (Before Web Store Submission):
- Add UI indication when CDP enforcement fails to attach (user awareness of actual protection level)
- One-time notice for existing users about WebSocket scope limitation
- Ensure CDP attach failure path has unit test (claimed in docs, verify exists)
NICE TO HAVE:
- Document cache format version policy as intentional drop-on-mismatch
- Add rate limiting to CDP event handler
- Consider in-extension onboarding for first-time CDP enforcement enablement
VERDICT
REQUEST_CHANGES
CRITICAL:
- ❌ Missing
normalizeBlockPatternimplementation in diff (input validation cannot be reviewed) - ❌ User input display in wizard (XSS risk) - need to see actual rendering code
HIGH:
⚠️ CDP enforcement fail-open lacks user visibility (silent degradation)⚠️ Debugger permission scope expansion may need CWS re-review flow⚠️ Cachefailurescounter lacks negative value guard
Summary:
The design is sound (fail-safe defaults, opt-in, per-tab scoping), and the real-browser validation (smoke:cdp) is excellent engineering. However, the diff is incomplete for security review - critical input validation code (normalizeBlockPattern) is referenced but not shown, and the wizard rendering path needs XSS verification.
Fix the CRITICAL findings, address the HIGH input validation issue, and re-submit. The cache version handling and CDP attach transparency are improvements but not blockers.
|
Thanks for the thorough pass. Dispositioning each finding against the code on the branch — several are already implemented as recommended, two CRITICAL/HIGH are false positives the diff proves safe, and the rest are low-severity or out-of-scope. Two independent adversarial reviews of this exact surface were run before the PR and drove the block-pattern hardening below. #1 HTML injection in wizard confirm step (CRITICAL) — not present. The confirm step builds every row with #2 Backslash authority bypass (CRITICAL) — fixed on this branch. #3 CDP detach race (HIGH) — already serialized (the recommended mitigation). #4 Block-pattern validation gaps (HIGH) — this is the core hardening of the PR. #5 WebSocket scope drift (HIGH) — guarded by an assertion. WebSocket is deliberately not closed by this layer (Fetch doesn't pause ws handshakes — ADR-007 Correction 3). #6 Kill-switch / external-driver reopen (HIGH) — documented boundary, not a regression. A consumer extension cannot categorically stop an external CDP driver (ADR-008); the README and store listing already state "a persistent external driver can reopen a tab." The kill switch closes the agent's tabs and tears down enforcement (validated in #7 Cache version downgrade (MEDIUM) — not attacker-reachable. #8 Backoff overflow (MEDIUM) — bounded. The exponent is #11 #12/#13 test coverage (MEDIUM) — present. #9/#10/#14 (MEDIUM, smoke-test/perf nits) — non-blocking. The smoke is a dev-only gate script (not shipped); its receipt log is bounded by the run; cache eviction is over ≤50 entries. Noted for follow-up, not release-blocking. Net: no finding reflects a defect in the shipped code. Dismissing to unblock; the substantive block-pattern hardening the review points at is exactly what this PR delivers, validated by unit tests, two adversarial passes, and a real-browser gate. |
Dispositioned point-by-point in the PR comment: findings are already-fixed (backslash, block-pattern validation), false positives proven safe by the cited tests (HTML injection — confirm step uses textContent + no-innerHTML lock-in), guarded non-issues (detach serialized by reconcileChain + attaching set; ws drift guarded by the smoke assertion), documented boundaries (external-driver reopen — ADR-008/README), or low-severity nits. No defect in shipped code; validated by 1043 unit tests, two adversarial passes, and smoke:cdp 26/26.
0.7.0 — CDP/debugger-layer enforcement of blocked domains, validated end-to-end
Takes ADR-007 CDP-layer network-egress enforcement from "built + unit-tested but never exercised in a real browser" to validated against a real Chrome, and folds the review-follow-up hardening — one coherent release.
What ships
chrome.debugger(CDPFetch), for tabs under an active delegation. Repatch-immune (a hostile page cannot restore it), fail-safe (any attach failure falls back to the page-realm interceptor; a fail-open decision path never hangs a request or breaks the user's own browsing), and torn down on delegation expiry/revoke, kill switch, tab close, or setting-off.npm run smoke:cdp):fetch/XHR, EventSource, element-src, Worker/iframe fetch, and navigation, to blocked domains.Fetchdomain does not pausews:///wss://handshakes (measured), and theNetwork.setBlockedURLsfallback raced the handshake and leaked intermittently on the live extension's shared debugger session. Rather than ship a flaky block behind a "closed" claim, the layer does not attempt it; a WebSocket to a blocked domain still connects, disclosed in the docs and store copy, with deterministic ws blocking deferred to ADR-008 R2 (declarativeNetRequest, a new permission). See ADR-007 Correction 3.Also in the batch
Gate on the exact artifact
npm run smoke:cdpALL GREEN — real headful Chrome on the builtdist/: default-off attaches nothing; the real popup wizard authors the block rule and the real settings toggle arms it; every CDP-closable vector is blocked while WebSocket stays open (asserted); blocks reported; navigation blocked; per-tab scope holds; and enforcement tears down on tab-close, kill switch, delegation-expiry-across-restart, and self-heals on restart.smoke:load/smoke:scope/smoke:scenariosgreensecure100/100 HARDENED on the artifact