Skip to content

feat: add Kernel browser extension with domain allowlist support#2

Open
ulziibay-kernel wants to merge 7 commits into
mainfrom
ulziibay/wba-allowlist
Open

feat: add Kernel browser extension with domain allowlist support#2
ulziibay-kernel wants to merge 7 commits into
mainfrom
ulziibay/wba-allowlist

Conversation

@ulziibay-kernel

@ulziibay-kernel ulziibay-kernel commented Apr 23, 2026

Copy link
Copy Markdown

Summary

  • Adds examples/kernel-browser-extension/ -- a Kernel-specific WBA extension that only signs main_frame navigation requests
  • Sub-resource requests (scripts, XHR, images, challenge iframes) pass through without signatures, preventing interference with bot-detection challenge flows (Cloudflare Turnstile, etc.)
  • The upstream examples/browser-extension/ is unchanged

Key differences from upstream extension

Upstream (browser-extension) Kernel (kernel-browser-extension)
Signing scope All requests to all URLs main_frame navigations only
Sub-resource signing Yes (scripts, XHR, images) No
Challenge flow interference Yes ("Incompatible browser extension" error) No
Config injection SIGNATURE_AGENT_URL Same

Why main_frame only?

Per the WBA RFC, the agent's "request" is the navigation it initiates. Sub-resource fetches (scripts, stylesheets, challenge JS) are the browser's internal behavior, not the agent's intent. Signing them causes Cloudflare's Turnstile verification to fail because the challenge orchestration requests at /cdn-cgi/challenge-platform/ get unexpected Signature headers.

Test results

Tested against web-bot-auth.org (Cloudflare free tier, Bot Fight Mode enabled, Worker origin) with headless Kernel browsers. Reproduced 3 times across separate runs:

Config Status cf-mitigated Challenged Saw origin
Headless, no WBA 403 challenge Yes No
Headless, with WBA 200 none No Yes

Test plan

  • Extension compiles with tsup, packages as CRX
  • Enterprise policy installs correctly on Kernel VMs
  • Signatures present on main_frame requests (verified via Playwright route intercept)
  • No signatures on sub-resource requests (scripts, XHR, images)
  • Cloudflare's research test site validates signatures as correct
  • Bot Fight Mode bypass confirmed: WBA sessions get 200, non-WBA get 403
  • No "Incompatible browser extension" errors on Cloudflare-protected sites (digikey.com)

Note

Low Risk
Adds an isolated example under examples/ with no changes to core libraries or the upstream extension; signing scope is intentionally narrower to reduce interference with third-party challenge flows.

Overview
Adds a new examples/kernel-browser-extension/ example: a Chrome MV3 extension that signs top-level navigations with RFC 9421 HTTP message signatures via web-bot-auth, packaged for enterprise force-install (CRX, update.xml, Chrome policy templates).

Signing behavior differs from the upstream browser-extension: the onBeforeSendHeaders handler only adds Signature / Signature-Input (and optional Signature-Agent) for main_frame requests, skips paths containing cdn-cgi/, and leaves sub-resources unsigned so challenge flows (e.g. Cloudflare Turnstile) are not broken.

Build pipeline uses tsup for the service worker, build_web_artifacts.mjs to inject SIGNATURE_AGENT_URL, stamp manifest version, pack CRX, and fill policy templates with the generated extension ID; private_key.pem and generated policy files are gitignored.

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

New extension at examples/kernel-browser-extension/ that only signs
requests matching an explicit domain allowlist, preventing interference
with challenge flows like Cloudflare Turnstile.

Key differences from the upstream examples/browser-extension/:
- Allowlist-based: only signs requests matching signDomains patterns
- Request type filtering via signTypes (default: main_frame, xmlhttprequest)
- Runtime config via chrome.storage.local (updateable via CDP)
- Build-time injection of SIGN_DOMAINS, SIGN_TYPES, SIGNATURE_AGENT_URL

Made-with: Cursor
The type filter alone wasn't sufficient because challenge orchestration
requests on the same domain (e.g. /cdn-cgi/challenge-platform/) were
being signed. Two fixes:

1. Default signTypes changed from ["main_frame","xmlhttprequest"] to
   ["main_frame"] -- only top-level navigations get signed by default
2. Added EXCLUDED_PATHS for /cdn-cgi/challenge-platform/ and
   /cdn-cgi/challenge/ as a safety net within allowlisted domains

Made-with: Cursor
@firetiger-agent

Copy link
Copy Markdown

Firetiger deploy monitoring skipped

This PR didn't match the auto-monitor filter configured on your GitHub connection:

Any PR that changes the kernel API. Monitor changes to API endpoints (packages/api/cmd/api/) and Temporal workflows (packages/api/lib/temporal) in the kernel repo

Reason: PR adds a browser extension in examples/ directory, not changes to API endpoints (packages/api/cmd/api/) or Temporal workflows (packages/api/lib/temporal) as specified in the filter.

To monitor this PR anyway, reply with @firetiger monitor this.

Comment thread examples/kernel-browser-extension/scripts/build_web_artifacts.mjs Outdated
Comment thread examples/kernel-browser-extension/src/background.ts Outdated
@ulziibay-kernel ulziibay-kernel requested a review from rgarcia April 27, 2026 19:04
The domain allowlist added unnecessary complexity. The core fix is
simpler: only sign main_frame navigation requests, which prevents
interference with challenge flows (Cloudflare Turnstile, etc.) while
signing every navigation the agent makes.

- Removed signDomains config and domain matching logic
- Removed signTypes config (hardcoded to main_frame)
- Removed chrome.storage.local runtime config
- Removed storage permission from manifest
- Removed SIGN_DOMAINS/SIGN_TYPES env var injection from build script
- Kept SIGNATURE_AGENT_URL injection (still needed)

Made-with: Cursor
},
{ urls: ["<all_urls>"] },
["blocking", "requestHeaders"],
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Domain allowlist filtering is completely missing

High Severity

The PR's core feature — domain allowlist support via signDomains glob patterns — is entirely absent from the implementation. The onBeforeSendHeaders listener only checks details.type !== "main_frame" but performs no domain filtering, so every main_frame request to every URL gets signed. The SIGN_DOMAINS and SIGN_TYPES env var injection described in the PR is also missing from build_web_artifacts.mjs, the chrome.storage.local runtime config code is absent, and the storage permission is not in manifest.json. The extension currently behaves identically to the upstream extension (minus sub-resource signing), defeating its stated purpose.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8c02137. Configure here.

Adds path-based filtering so any request whose URL path contains
"cdn-cgi/" is passed through without a Signature. Cloudflare's challenge
orchestration (Turnstile, challenge-platform) lives under /cdn-cgi/, and
signing those requests breaks the challenge flow with "Incompatible
browser extension". Layers on top of the existing main_frame-only rule
as a safety net for top-level navigations that land on a /cdn-cgi/ URL.

Made-with: Cursor

@cursor cursor Bot left a comment

Copy link
Copy Markdown

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 using high effort and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

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 536ac81. Configure here.


this.privateKey = fullSecretKey;
this.keyid = KEY_ID;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Key ID set asynchronously

Medium Severity

KEY_ID is initialized to "not-set-yet" and updated only after async jwkToKeyID resolves, while each signed request builds a new Ed25519Signer that copies the current KEY_ID synchronously. Early navigations can emit signatures verifiers reject.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 536ac81. Configure here.

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.

1 participant