feat(extension): local security checks for the active page - #240
Merged
Merged
Conversation
…mixed content locally The popup gets a "This page" view with pass/warn/fail rows and the toolbar badge is set per tab. Checks run in the browser: main-frame response headers observed with webRequest (HSTS, CSP incl. unsafe-inline/unsafe-eval/wildcard analysis, framing, nosniff, Referrer-Policy, Permissions-Policy), cookie flags via chrome.cookies (names only), and a DOM/Resource Timing collector for mixed content and insecure forms. Site access is an optional host permission requested at first use; nothing is sent to a server unless the user clicks "Scan with ThreatCrush", which sends origin+path only. Also fixes account calls that read a never-written `authToken`/`session` key (now the Supabase session token), a popup crash when the build has no Supabase config, and runs the extension tests and build in PR checks.
ThreatCrush Security Scan12 finding(s) HIGH/CRITICAL: 1 | MEDIUM: 6 | LOW: 5
Snippets are redacted; ThreatCrush never prints matched credential material. |
ralyodio
added a commit
that referenced
this pull request
Sep 25, 2026
ralyodio
added a commit
that referenced
this pull request
Sep 25, 2026
…refresh release docs (#238) * ci(desktop): sign and notarize when secrets exist; declare libgbm1/ALSA in the deb - scripts/desktop-signing-env.sh exports electron-builder's signing variables only for secrets that are set (APPLE_CERTIFICATE[_PASSWORD], APPLE_API_KEY/ _KEY_ID/_ISSUER or APPLE_ID/APPLE_APP_SPECIFIC_PASSWORD/APPLE_TEAM_ID, WINDOWS_CERTIFICATE[_PASSWORD]); without them builds stay unsigned - drop notarize: false so notarization follows the credentials - package (never publish) the desktop matrix on PRs touching desktop packaging - deb.depends adds libgbm1 and libasound2t64 | libasound2: the published deb failed on minimal installs with libgbm.so.1 missing - docs: desktop packaging has been green since v0.13.2; refresh release, surface and mobile status from gh evidence - web: Download Desktop links pointed at a 404 /releases; state that macOS/Windows builds are unsigned * ci(desktop): leave pull-request packaging runs unsigned electron-builder already skips macOS signing on PRs (CSC_FOR_PULL_REQUEST); treat Windows the same so PR runs never decode the certificates, and say so in the job summary instead of claiming signing is on. * docs: record extension and libinjection status; soften unsigned-build wording SURFACES.md picks up the browser-extension (#240) and libinjection/WASM (#239) status lines and the extension-store blockers; RELEASE_STATUS.md lists the store accounts. The homepage now says unsigned macOS/Windows builds may be blocked, not only warned about, on first launch. * docs: mark the extension page checks (#240) as merged in surface and release status
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The browser extension now runs real security checks on the page you're viewing, in the browser:
max-age=0/ < 180 days; n/a on http)'unsafe-inline'(unless nonce/hash),'unsafe-eval',*/https:/data:(ignored under'strict-dynamic'); multiple policies intersected;<meta>fallbackframe-ancestors, elseX-Frame-Options<meta name=referrer>), Permissions-Policyformaction)chrome.cookiesautoScansetting is gone./api/scanserver scan) moved into the page view. It is the only thing that sends page data anywhere, runs only on click, sends origin + path only (query string and fragment dropped), and the popup shows the exact URL first. No sign-in required (the endpoint is public).apps/web/.../api/scan/route.tsis unchanged.Code: pure checks in
src/lib/page-checks.js, in-page collector insrc/lib/collect-page.js(injected withscripting.executeScript), orchestration insrc/background/page-checks.js.Permissions (and why)
permissions:activeTab,scripting,webRequest,cookies. None of these adds a Chrome install warning on its own.optional_host_permissions: ["http://*/*", "https://*/*"], requested at first use from the popup's "Enable page checks" button or from Options, and revocable there.webRequestandcookiesonly see hosts we have access to. Response headers have to be observed while the page loads, andactiveTabis only granted after that (on the toolbar click), so per-click access can't capture them. Making the access optional keeps the install prompt clean and leaves the choice to the user. Without it, the HTTPS/forms/mixed-content checks still run viaactiveTabwhen the popup opens; header and cookie rows show "Unknown" with the reason.optional_permissions: ["tabs", "activeTab"].activeTabonly works when it's a required permission.webRequestis used observationally only (onHeadersReceived,types: ["main_frame"],["responseHeaders"]). Only the 8 security headers are kept, instorage.sessionper tab, and they're removed when the tab closes.Firefox MV3 differences (handled / documented in the README)
Host permissions are always user-controlled;
optional_host_permissionsneeds Fx 128+ (the manifest already requires 142+). The background is an event page, so listeners are registered at top level.permissions.request()is called synchronously in the click handler.cookies.getAllretries withfirstPartyDomain: nullunder first-party isolation.data_collection_permissionsstaysnone: the scan button is user-initiated, clearly labelled, and sends only the current page URL, which is implied consent under AMO policy §6.2.2.2.Fixes found along the way
GET_STATSread anauthTokenkey that nothing ever wrote, andapi.jsread asessionkey only the unusedapi.login()wrote. Signed-in users therefore always got zero stats. Requests now carry the Supabase session's access token (which/api/usageverifies). Removed the deadapi.login/api.logout.createClient('')throws at import when a build has no Supabase env, which crashed the whole popup. The client is nownullin that case: sign-in says it isn't configured and page checks still work.How verified
pnpm --filter @profullstack/threatcrush-extension test: 5 files, 53 tests.page-checks.test.jshas pass and fail cases for every check: headers→findings, CSP analysis, framing precedence, cookie flags, mixed content, forms, report assembly, badge, and scan URL stripping.collect-page.test.jsruns the DOM collector in jsdom.auth-token.test.jsholds regression tests that fail on master ("expected undefined to be 'Bearer …'", "supabaseUrl is required.") and pass here.pnpm --filter @profullstack/threatcrush-extension build(chrome/firefox/safari).web-ext linton the Firefox build: 0 errors, the same 6 warnings as master (1×1 placeholder icons, react-dom innerHTML).--load-extension). Local http and self-signed https servers, reached asinsecure.test/weak.test/secure.testvia--host-resolver-rules. Popup opened withchrome.action.openPopup()and read over CDP:http://insecure.test/insecure(password form,sessionidcookie, no headers): badge 2 (red). HTTPS fail, forms fail ("Password field on a page served over plain HTTP"), cookies warnsessionid: no Secure, no HttpOnly, no SameSite, HSTS/mixed n/a.https://weak.test/weak(HSTS 3600s, CSP with'unsafe-inline' 'unsafe-eval', XFO ALLOW-FROM,Referrer-Policy: unsafe-url, http script+img, form → http,auth_tokencookie without flags): badge 5. 5 fail / 4 warn / 1 pass, each row as expected.https://secure.test/strong: badge ✓, 10/10 pass.https://example.com/?token=abc#frag: 1 fail (HSTS) / 5 warn / 4 pass. Clicking Scan hit production/api/scanand showed "Grade F · Score 25/100". The caption showedhttps://example.com/.POST /api/scan {"url":"https://weak.test:18443/weak"}for…/weak?session=abc#x.chrome.permissions.request({origins:["http://*/*","https://*/*"]}). Options shows "Site access not granted/granted" per build. The Account tab shows the login form (or "not configured" without Supabase env), and signed-outGET_STATSreturns zeros.Not exercised: accepting the native permission prompt (headless can't click it; the smoke used a copy of the build with the origins moved to
host_permissions), theactiveTab-only DOM path (needs a real toolbar click), a signed-in account, and Firefox and Safari at runtime (no Firefox or Safari on the test machine).Blocked on accounts (not code)
WEB_EXT_API_KEY/WEB_EXT_API_SECRET) forweb-ext sign.