Skip to content

Add domain-firewall skill — CDP navigation security for browser agents#63

Open
aq17 wants to merge 2 commits intomainfrom
add-domain-firewall-skill
Open

Add domain-firewall skill — CDP navigation security for browser agents#63
aq17 wants to merge 2 commits intomainfrom
add-domain-firewall-skill

Conversation

@aq17
Copy link
Copy Markdown
Contributor

@aq17 aq17 commented Mar 30, 2026

Summary

Adds a domain-firewall skill that protects Browserbase (and local Chrome) sessions from unauthorized navigations at the Chrome DevTools Protocol level. Designed to prevent prompt injection attacks that trick AI agents into navigating to attacker-controlled URLs.

Two interfaces:

  • CLI script (agent-first) — node domain-firewall.mjs --session-id <id> --allowlist "example.com" attaches to a live session and enforces policies transparently. Works with the browse CLI, Stagehand, or any CDP client.
  • TypeScript API (code integration) — installDomainFirewall(page, { policies, defaultVerdict }) for embedding directly in Stagehand projects with composable policies.

What it does

  • Intercepts all Document navigations via Fetch.requestPaused CDP event
  • Evaluates requests against a policy chain: denylist → allowlist → default verdict
  • Blocks unauthorized navigations with Fetch.failRequest("BlockedByClient") — Chrome shows ERR_BLOCKED_BY_CLIENT
  • Logs all decisions (allowed/blocked + which policy decided) to stdout or audit trail

Built-in policies (TypeScript API)

Policy Purpose
allowlist(domains) Static domain allowlist
denylist(domains) Static domain denylist
pattern(globs, verdict) Glob matching on domains
tld(rules) TLD-based rules (.org, .edu, .ru)
interactive(handler) Human-in-the-loop approval with session memory

CLI script flags

--session-id <id>      Browserbase session ID
--cdp-url <ws://...>   Direct CDP WebSocket URL (local Chrome)
--allowlist <domains>   Comma-separated allowed domains
--denylist <domains>    Comma-separated denied domains
--default <verdict>     allow or deny (default: deny)
--json                  Output events as JSON lines
--quiet                 Suppress per-request logging

Security demo results

Tested against a honeypot page containing hidden prompt injection that directs agents to an attacker URL:

Scenario Result
Without firewall Agent navigated to attacker URL, session token exfiltrated
With firewall (--allowlist localhost) Navigation blocked at CDP level, attacker received zero data

Files

skills/domain-firewall/
├── SKILL.md                          # Skill docs (threat model, policies, usage)
├── REFERENCE.md                      # API reference, CDP protocol details
├── EXAMPLES.md                       # 6 usage examples
├── LICENSE.txt                       # MIT
├── package.json                      # ws dependency for CDP WebSocket
└── scripts/
    └── domain-firewall.mjs           # CLI-native firewall script (agent-first)

Test plan

  • CLI script connects to Browserbase sessions via bb sessions debug
  • CLI script connects to local Chrome via --cdp-url
  • Allowlisted domains pass through (Fetch.continueRequest)
  • Non-allowlisted domains blocked (Fetch.failRequest)
  • Denylist takes priority over allowlist
  • Browser-level CDP auto-attaches to page target (coexists with browse CLI)
  • Fail-closed on policy errors (no hung requests)
  • Graceful shutdown on SIGINT
  • JSON output mode (--json)
  • Honeypot attack demo: unprotected agent gets pwned, protected agent blocks exfiltration

🤖 Generated with Claude Code


Note

Medium Risk
Adds a new CDP WebSocket client that can block browser navigations at the protocol level; mistakes in interception/attachment logic could break browsing sessions or block legitimate navigations.

Overview
Adds a new domain-firewall skill to the marketplace, providing a CLI that connects to a Browserbase session (via bb sessions debug) or a local Chrome CDP URL and enforces domain-based navigation controls.

The new domain-firewall.mjs intercepts Document navigations using CDP Fetch.enable/Fetch.requestPaused, applies denylist → allowlist → default verdict decisions (including *.domain wildcard matching), and logs decisions in human-readable or JSON modes with graceful shutdown handling.

Includes packaged dependencies (ws) plus reference docs and example usage covering high-risk automation scenarios.

Reviewed by Cursor Bugbot for commit 245e377. Bugbot is set up for automated code reviews on this repo. Configure here.

@aq17 aq17 requested review from Kylejeong2, shrey150 and shubh24 March 30, 2026 21:23
@aq17
Copy link
Copy Markdown
Contributor Author

aq17 commented Mar 30, 2026

Inspired by @shubh24 's sample script for Notion who raised this as a top-of-mind guardrail for security

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Missing await on sendCDP calls in event handler
    • I confirmed the missing awaits in installDomainFirewall and added await to all page.sendCDP calls in the Fetch.requestPaused handler to prevent unhandled rejections and stuck paused requests.

Create PR

Or push these changes by commenting:

@cursor push 36f6f7fbd8
Preview (36f6f7fbd8)
diff --git a/skills/domain-firewall/SKILL.md b/skills/domain-firewall/SKILL.md
--- a/skills/domain-firewall/SKILL.md
+++ b/skills/domain-firewall/SKILL.md
@@ -300,7 +300,7 @@
       // Pass through non-document requests (images, CSS, JS, fonts, etc.)
       const resourceType = params.resourceType || "";
       if (resourceType !== "Document" && resourceType !== "") {
-        page.sendCDP("Fetch.continueRequest", { requestId: params.requestId });
+        await page.sendCDP("Fetch.continueRequest", { requestId: params.requestId });
         return;
       }
 
@@ -310,7 +310,7 @@
         url.startsWith("about:") ||
         url.startsWith("data:")
       ) {
-        page.sendCDP("Fetch.continueRequest", { requestId: params.requestId });
+        await page.sendCDP("Fetch.continueRequest", { requestId: params.requestId });
         return;
       }
 
@@ -318,7 +318,7 @@
       try {
         domain = normalizeDomain(new URL(url).hostname);
       } catch {
-        page.sendCDP("Fetch.continueRequest", { requestId: params.requestId });
+        await page.sendCDP("Fetch.continueRequest", { requestId: params.requestId });
         return;
       }
 
@@ -334,13 +334,13 @@
           time: ts(), domain, url: url.substring(0, 80),
           action: "ALLOWED", decidedBy,
         });
-        page.sendCDP("Fetch.continueRequest", { requestId: params.requestId });
+        await page.sendCDP("Fetch.continueRequest", { requestId: params.requestId });
       } else {
         auditLog?.push({
           time: ts(), domain, url: url.substring(0, 80),
           action: "BLOCKED", decidedBy,
         });
-        page.sendCDP("Fetch.failRequest", {
+        await page.sendCDP("Fetch.failRequest", {
           requestId: params.requestId,
           errorReason: "BlockedByClient",
         });

This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

Comment thread skills/domain-firewall/SKILL.md Outdated
Comment thread skills/domain-firewall/SKILL.md Outdated
Comment thread skills/domain-firewall/SKILL.md Outdated
Comment thread skills/domain-firewall/SKILL.md Outdated
@aq17 aq17 closed this Apr 3, 2026
@aq17 aq17 reopened this Apr 3, 2026
@aq17 aq17 changed the title Add domain-firewall skill (CDP navigation security via Stagehand) Add domain-firewall skill — CDP navigation security for browser agents Apr 3, 2026
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 3 total unresolved issues (including 2 from previous reviews).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Case-sensitive www prefix strip before lowercasing
    • Updated domain normalization to lowercase before stripping the www prefix in both the runtime script and SKILL example code.

Create PR

Or push these changes by commenting:

@cursor push e179cf303d
Preview (e179cf303d)
diff --git a/skills/domain-firewall/SKILL.md b/skills/domain-firewall/SKILL.md
--- a/skills/domain-firewall/SKILL.md
+++ b/skills/domain-firewall/SKILL.md
@@ -180,7 +180,7 @@
 
 ```typescript
 function normalizeDomain(hostname: string): string {
-  return hostname.replace(/^www\./, "").toLowerCase();
+  return hostname.toLowerCase().replace(/^www\./, "");
 }
 
 function ts(): string {

diff --git a/skills/domain-firewall/scripts/domain-firewall.mjs b/skills/domain-firewall/scripts/domain-firewall.mjs
--- a/skills/domain-firewall/scripts/domain-firewall.mjs
+++ b/skills/domain-firewall/scripts/domain-firewall.mjs
@@ -95,7 +95,7 @@
 // =============================================================================
 
 function normalizeDomain(hostname) {
-  return hostname.replace(/^www\./, "").toLowerCase();
+  return hostname.toLowerCase().replace(/^www\./, "");
 }
 
 function ts() {

This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

Comment thread skills/domain-firewall/scripts/domain-firewall.mjs
Comment thread skills/domain-firewall/scripts/domain-firewall.mjs Outdated
Comment thread skills/domain-firewall/scripts/domain-firewall.mjs
Comment thread skills/domain-firewall/scripts/domain-firewall.mjs Outdated
Comment thread skills/domain-firewall/scripts/domain-firewall.mjs Outdated
Comment thread skills/domain-firewall/scripts/domain-firewall.mjs Outdated
Comment thread skills/domain-firewall/scripts/domain-firewall.mjs
Comment thread skills/domain-firewall/scripts/domain-firewall.mjs
Comment thread skills/domain-firewall/scripts/domain-firewall.mjs Outdated
@aq17 aq17 requested a review from shubh24 April 6, 2026 23:21
Comment thread skills/domain-firewall/scripts/domain-firewall.mjs Outdated
@aq17
Copy link
Copy Markdown
Contributor Author

aq17 commented Apr 7, 2026

Potential messaging:

Your browser agent just got prompt injected. One hidden link on a page, and your session
   token is on its way to an attacker.

  We built a fix: domain-firewall. One CLI command, zero code changes. It intercepts every
   navigation at the Chrome DevTools Protocol level — before the request ever leaves the
  browser.

  node domain-firewall.mjs --session-id <id> --allowlist "stripe.com" --default deny

  Not on the allowlist? Killed instantly. No bytes reach the attacker.

  We tested it: same page, same malicious link. Without the firewall — data exfiltrated.
  With it — ERR_BLOCKED_BY_CLIENT. Done.

  Open source, ships as a Browserbase skill. Go protect your agents.

How it works

  1. Agent creates a Browserbase session via bb sessions create
  2. Agent runs node domain-firewall.mjs --session-id <id> --allowlist "example.com"
  --default deny
  3. The script connects to the session's CDP WebSocket (resolved via bb sessions debug)
  4. It sends Fetch.enable to intercept all network requests at the protocol level
  5. On every Fetch.requestPaused event, it checks the domain against the denylist →
  allowlist → default verdict
  6. Allowed: Fetch.continueRequest. Blocked: Fetch.failRequest("BlockedByClient")
  7. Logs every decision to stdout. Runs until Ctrl+C or session ends.

Comment thread skills/domain-firewall/scripts/domain-firewall.mjs Outdated
Comment thread skills/domain-firewall/scripts/domain-firewall.mjs
@aq17 aq17 force-pushed the add-domain-firewall-skill branch from 93df23b to df68c27 Compare April 24, 2026 21:35
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit df68c27. Configure here.

Comment thread skills/domain-firewall/scripts/domain-firewall.mjs
Comment thread skills/domain-firewall/scripts/domain-firewall.mjs Outdated
CLI-first skill that protects Browserbase and local Chrome sessions
from prompt injection, malicious redirects, and data exfiltration
by intercepting navigations at the Chrome DevTools Protocol level.

Includes:
- SKILL.md: setup, quick start, agent workflow, CLI reference,
  best practices, troubleshooting
- REFERENCE.md: TypeScript API with composable policy system
  (allowlist, denylist, pattern, tld, interactive)
- EXAMPLES.md: 8 real-world use cases (banking, CRM migration,
  procurement, e-commerce, staging isolation, etc.)
- marketplace.json: plugin registration with security category

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@aq17 aq17 force-pushed the add-domain-firewall-skill branch from df68c27 to 4663e68 Compare April 24, 2026 21:57
Verified live: with urlPattern: "*" a single wikipedia nav took 29
Fetch.requestPaused round-trips (1 Document + 28 sub-resources); the
handler pattern-matched and immediately continued all 28. Filtering at
the CDP layer drops them before they hit our WS, no behavior change
(handler already policed Documents only).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants