Skip to content

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

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

feat: add Kernel browser extension with domain allowlist support#2
ulziibay-kernel wants to merge 9 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

Medium Risk
New example ships signing keys in-repo and a broad <all_urls> webRequest hook; behavior is scoped to main_frame but still affects outbound HTTP identity on navigations.

Overview
Adds examples/kernel-browser-extension/, a Kernel-focused Web Bot Auth MV3 Chrome extension that signs only main_frame navigations and leaves sub-resources (XHR, scripts, challenge iframes) unsigned so bot-challenge flows are not broken.

The service worker adds RFC 9421 Signature / Signature-Input (and optional Signature-Agent) via web-bot-auth, with Ed25519 signing through libsodium. It can rewrite the User-Agent header on signed navigations (default KernelSearchBot, disable via build-time USER_AGENT_OVERRIDE).

bundle:chrome builds the extension, packs a CRX, writes update.xml, generates private_key.pem, and renders Chrome enterprise policy templates (policy.json, macOS plist) with the computed extension ID for force-install from a local update server.

build_web_artifacts.mjs injects SIGNATURE_AGENT_URL and USER_AGENT_OVERRIDE into the compiled background script at bundle time.

Reviewed by Cursor Bugbot for commit 51ade52. 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

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.

@rgarcia rgarcia 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.

reviewed and approved — a few follow-ups worth addressing:

correctness and build

  • examples/kernel-browser-extension/package.json:2 — update the root package-lock.json for this new workspace. npm ci --ignore-scripts currently fails with Missing: kernel-web-bot-auth-extension@0.3.0 from lock file.
  • examples/kernel-browser-extension/package.json:6-7,27-33 — the source imports web-bot-auth, but the package neither declares it as a dependency nor ensures its workspace build output exists. After compensating for the stale lockfile, a clean bundle:chrome failed because packages/web-bot-auth/dist/index.mjs was missing. Consider declaring the dependency and building its workspace prerequisites first.
  • examples/kernel-browser-extension/src/background.ts:17-49 — cold-start requests can reach Ed25519Signer before its asynchronous prerequisites are ready: KEY_ID can still be "not-set-yet", and sodium is used without waiting for _sodium.ready. Since the listener must remain synchronous, consider embedding the deterministic thumbprint and ensuring sodium initialization finishes before requests are eligible for signing.
  • examples/kernel-browser-extension/scripts/build_web_artifacts.mjs:78-110 — every bundle generates a new CRX signing key, so the extension ID changes on every build. Consider reusing private_key.pem when present or requiring a stable key through configuration so rebuilt artifacts can update existing installations.
  • examples/kernel-browser-extension/scripts/build_web_artifacts.mjs:67-68 — if SIGNATURE_AGENT_URL is supplied but injection fails, consider throwing instead of continuing to package a misconfigured extension.

behavior and policy

  • examples/kernel-browser-extension/policy/com.google.Chrome.managed.plist.templ:7-11 — is blocking every extension other than this one intentional? The macOS policy differs from the JSON policy and could disable unrelated managed extensions.
  • examples/kernel-browser-extension/src/background.ts:58-62pathname.includes("cdn-cgi/") also skips unrelated routes containing that substring. Consider matching pathname.startsWith("/cdn-cgi/").
  • examples/kernel-browser-extension/src/background.ts:68-72,107 — add types: ["main_frame"] to the registration filter so scripts, images, XHRs, and iframes bypass the blocking listener entirely, matching the PR's stated intent.

docs, tests, and nits

  • examples/kernel-browser-extension/package.json:4 — the description and PR title mention an allowlist, but the final implementation signs eligible top-level requests globally. Update the wording or restore the allowlist behavior.
  • examples/kernel-browser-extension/package.json:9 — the test command always fails and the new request-selection behavior has no automated coverage. At minimum, consider focused tests for main-frame filtering and /cdn-cgi/ exclusion.
  • examples/kernel-browser-extension/ — consider adding a README and linking it from the root example table. The build order, SIGNATURE_AGENT_URL, generated policies, local update server, and main-frame-only behavior otherwise exist only in scripts or the PR description.
  • examples/kernel-browser-extension/src/background.ts — nit: npx prettier --check examples/kernel-browser-extension currently fails on this file.

ulziibay-kernel and others added 2 commits July 23, 2026 08:49
…quests

Rewrite the User-Agent header to KernelSearchBot on signed main_frame
navigations (before signing, so signed and sent headers stay consistent).
Overridable at build time via USER_AGENT_OVERRIDE. Temporary until the UA is
set natively in the browser image; only affects the HTTP header, not
navigator.userAgent.

Co-authored-by: Cursor <cursoragent@cursor.com>

@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 3 total unresolved issues (including 2 from previous reviews).

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 51ade52. Configure here.

const message = sodium.from_string(data);
const signedMessage = sodium.crypto_sign(message, this.privateKey);
return signedMessage.slice(0, sodium.crypto_sign_BYTES);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Crypto used before libsodium ready

High Severity

The extension never awaits _sodium.ready before from_base64 / crypto_sign in the blocking onBeforeSendHeaders path. After a service-worker restart, the first main_frame sign can throw while WASM is still initializing, so the navigation goes out unsigned or fails outright.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 51ade52. 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.

2 participants