feat: add Kernel browser extension with domain allowlist support#2
feat: add Kernel browser extension with domain allowlist support#2ulziibay-kernel wants to merge 9 commits into
Conversation
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
…only Made-with: Cursor
Made-with: Cursor
|
Firetiger deploy monitoring skipped This PR didn't match the auto-monitor filter configured on your GitHub connection:
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 |
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
Made-with: Cursor
| }, | ||
| { urls: ["<all_urls>"] }, | ||
| ["blocking", "requestHeaders"], | ||
| ); |
There was a problem hiding this comment.
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)
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; | ||
| } |
There was a problem hiding this comment.
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)
Reviewed by Cursor Bugbot for commit 536ac81. Configure here.
rgarcia
left a comment
There was a problem hiding this comment.
reviewed and approved — a few follow-ups worth addressing:
correctness and build
examples/kernel-browser-extension/package.json:2— update the rootpackage-lock.jsonfor this new workspace.npm ci --ignore-scriptscurrently fails withMissing: kernel-web-bot-auth-extension@0.3.0 from lock file.examples/kernel-browser-extension/package.json:6-7,27-33— the source importsweb-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 cleanbundle:chromefailed becausepackages/web-bot-auth/dist/index.mjswas missing. Consider declaring the dependency and building its workspace prerequisites first.examples/kernel-browser-extension/src/background.ts:17-49— cold-start requests can reachEd25519Signerbefore its asynchronous prerequisites are ready:KEY_IDcan 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 reusingprivate_key.pemwhen 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— ifSIGNATURE_AGENT_URLis 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-62—pathname.includes("cdn-cgi/")also skips unrelated routes containing that substring. Consider matchingpathname.startsWith("/cdn-cgi/").examples/kernel-browser-extension/src/background.ts:68-72,107— addtypes: ["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-extensioncurrently fails on this file.
This reverts commit 536ac81.
…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>
There was a problem hiding this comment.
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).
❌ 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); | ||
| } |
There was a problem hiding this comment.
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)
Reviewed by Cursor Bugbot for commit 51ade52. Configure here.


Summary
examples/kernel-browser-extension/-- a Kernel-specific WBA extension that only signsmain_framenavigation requestsexamples/browser-extension/is unchangedKey differences from upstream extension
browser-extension)kernel-browser-extension)main_framenavigations onlySIGNATURE_AGENT_URLWhy 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 unexpectedSignatureheaders.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:Test plan
main_framerequests (verified via Playwright route intercept)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 onlymain_framenavigations 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 optionalSignature-Agent) viaweb-bot-auth, with Ed25519 signing through libsodium. It can rewrite theUser-Agentheader on signed navigations (defaultKernelSearchBot, disable via build-timeUSER_AGENT_OVERRIDE).bundle:chromebuilds the extension, packs a CRX, writesupdate.xml, generatesprivate_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.mjsinjectsSIGNATURE_AGENT_URLandUSER_AGENT_OVERRIDEinto 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.