Version 1.0.0 · Changelog · Source: https://github.com/Subnet345LLC/FixClick
A cross-browser Manifest V3 extension for Chrome, Edge, Brave, Opera and other Chromium browsers, plus Firefox, that detects two credential/token-theft attack families in the page, in real time, and warns the user:
- ClickFix — fake "human verification" / error pages that trick you into pasting and running a command in the Windows Run dialog, PowerShell, or a terminal. Detected via clipboard interception + on-page lure heuristics.
- Ghost phishing — Microsoft credential/token theft that appears to run on
Microsoft's own infrastructure:
- AiTM — a page rendering Microsoft's cloud sign-in form (the
loginfmtusername field) from a non-Microsoft origin (reverse-proxy kits such as Evilginx / EvilProxy). Keying onloginfmtavoids flagging legitimate on-prem AD FS pages and "Sign in with Microsoft" buttons, which don't render that field. - Device-code — the genuine device-authorization page, where an attacker relays a code for you to authorize their device.
- Consent — an OAuth consent screen requesting high-risk Graph scopes and/or a non-Microsoft redirect.
- AiTM — a page rendering Microsoft's cloud sign-in form (the
- Manifest V3, no bundler and no runtime dependencies — the build is a small dependency-free Node script that assembles the per-target manifest.
- Extensible: every attack family is a detector module implementing a shared
lifecycle interface and registered in one place. Adding attack #3 = one new
file under
src/detectors/+ oneregister()line + one manifest entry.
manifests/ manifest.config.mjs (shared base + per-target diffs; one source for both)
scripts/ build.mjs (dependency-free packager: src + icons + assembled manifest → dist/<target>)
src/ SHARED source, identical across browsers
shared/ constants.js · microsoft.js · patterns.js (single source of truth)
core/ finding.js (factory) · registry.js
detectors/ base-detector.js · clickfix-detector.js · ghostphishing-detector.js
ui/ overlay.js (shadow-DOM banner + blocking interstitial, top frame only)
content/ main-world-hook.js (MAIN world, clipboard wrap)
content-orchestrator.js (isolated world, wires it together)
background/ service-worker.js (findings ring buffer, per-tab badge, settings)
popup/ popup.html · popup.css · popup.js
tests/ clickfix-sample.html · aitm-sample.html · cdn-only-negative.html
assets/store/ listing copy, privacy policy, and generated screenshots
dist/ build output (git-ignored): dist/chrome · dist/firefox
Store submission assets live in assets/store/: listing.md (name, summary,
description, category, permission justifications), privacy-policy.md, and three
1280×800 screenshots regenerated with npm run screenshots (they drive the real
extension against the fixtures — authentic, not mockups).
One source, two targets: src/ and icons/ are shared verbatim, and the
manifest is assembled from a single manifests/manifest.config.mjs — a shared
base plus a small per-target override (Chrome uses background.service_worker
minimum_chrome_version; Firefox usesbackground.scripts+browser_specific_settings.gecko). The content-scriptjslists are declared once inbase, so there is nothing to keep in sync. The code useschrome.*with callbacks, which Firefox aliases — so nowebextension-polyfillis needed.world: "MAIN"content scripts require Chrome 111+ / Firefox 128+; the Firefox build targets 140+ (current ESR) for thedata_collection_permissionskey.
Two content-script worlds:
- MAIN world (
main-world-hook.js) runs atdocument_startand wrapsnavigator.clipboard.writeText,document.execCommand('copy'), andDataTransfer.setData. It relays clipboard writes to the isolated world viawindow.postMessage— it contains no detection logic. - Isolated world (the ordered
jslist) shares one sandbox scope. Files attach to a singleglobalThis.FixClicknamespace and load in dependency order, withcontent-orchestrator.jslast as the entry point.
npm run build # builds dist/chrome and dist/firefox
npm run build:chrome # or a single target
npm run build:firefox
npm run check # validate manifests + JS syntax
npm run icons # regenerate icons/*.png from the vector definition
The icon set (a red security shield with a cursor arrow) is defined as a single
SVG in scripts/make-icons.mjs and rasterized to 16/32/48/128 px via headless
Chromium.
Chrome / Edge / any Chromium browser (requires Chromium 111+):
npm run build:chrome- Go to
chrome://extensions(oredge://extensions). - Enable Developer mode → Load unpacked → select
dist/chrome.
Firefox (requires Firefox 140+):
-
npm run build:firefox -
Go to
about:debugging#/runtime/this-firefox→ Load Temporary Add-on → selectdist/firefox/manifest.json. (Or, with theweb-exttool:web-ext run -s dist/firefox.)Note: under Firefox MV3,
<all_urls>host access is user-controlled — you may need to grant it via the extension's permissions to enable page scanning.
Two layers, both runnable from the CLI:
npm test # fast logic harness (no browser): drives detectors against
# crafted inputs and asserts on emitted findings (26 checks)
npm run test:e2e # Playwright: loads dist/chrome in real Chromium and asserts
# the clipboard hook, overlay/interstitial, input disabling,
# finding persistence, and popup toggle (6 tests)
npm run test:e2e:firefox # Selenium + geckodriver: loads dist/firefox in real
# headless Firefox and asserts the banner, blocking
# interstitial, input disabling, and negative case
npm run lint:firefox # web-ext (AMO's linter) validates dist/firefox
npm run run:firefox # launch Firefox with the extension loaded (manual testing)
Chrome E2E uses Playwright (channel: 'chromium', new headless). Firefox E2E
uses Selenium WebDriver — Playwright can't load Firefox extensions. Both are
driven by the same fixtures and shared static server (e2e/static-server.mjs).
Selenium Manager auto-provisions geckodriver (and can download Firefox itself if
none is installed).
The extension code binds to globalThis.browser ?? globalThis.chrome, so the
same source uses promise-based APIs on both engines (Firefox browser.*, Chrome
MV3 chrome.*) with no polyfill. The overlay and popup build their DOM via
textContent (no innerHTML), so there is no HTML-injection surface.
npm run lint:firefox reports 0 errors; the one remaining warning is
Firefox-for-Android compatibility, which is expected — FixClick targets desktop
browsers. The Firefox build also declares data_collection_permissions: none
(all analysis is on-device; nothing is transmitted).
npm run test:e2e requires a one-time browser download: npx playwright install chromium. Set HEADED=1 to watch it run. Playwright is a dev-only dependency
and is never bundled into dist/. The overlay uses a closed shadow root, so
the E2E tests assert on the light-DOM host elements + the findings recorded in
chrome.storage (read directly from the service worker) rather than shadow
contents.
Serve the fixtures over HTTP (content scripts don't run on file:// unless you
enable "Allow access to file URLs" for the extension):
cd tests
python3 -m http.server 8080
- ClickFix: open
http://localhost:8080/clickfix-sample.html, click a Copy button → a warning banner appears; the toolbar badge increments. - AiTM: open
http://localhost:8080/aitm-sample.html→ a CRITICAL fake Microsoft sign-in warning appears (Microsoft cloudloginfmtform on a non-MS origin). With hard-block on (default) this is a full-screen interstitial that also disables the credential fields; toggle it off in the popup to get a banner. - Negative case: open
http://localhost:8080/cdn-only-negative.html→ no finding. It references the Microsoft auth CDN and shows branding + a "Sign in with Microsoft" button but renders nologinfmtfield, so the tightened AiTM check correctly stays silent.
Open the toolbar popup to see recent detections and toggle detectors on/off.
- Create
src/detectors/my-detector.js:;(function () { const NS = (globalThis.FixClick = globalThis.FixClick || {}); class MyDetector extends NS.BaseDetector { constructor() { super({ id: 'my-attack', name: '…', attackType: '…' }); } onInit(ctx) {} // URL / query checks analyzePage(ctx) {} // DOM checks (load + mutations) onClipboardWrite(ctx) {}// ctx.text } NS.MyDetector = MyDetector; })();
- Add it to the
ISOLATED_JSlist inmanifests/manifest.config.mjs(beforecontent-orchestrator.js) — one edit, applies to every target. registry.register(new NS.MyDetector());incontent-orchestrator.js.npm run build.
- Heuristic detection can produce false positives/negatives. Non-critical findings show a dismissible banner. Critical findings (currently AiTM) show a full-screen blocking interstitial and disable the credential fields behind it; the user can leave the page or explicitly override. This "hard-block" behaviour is on by default and toggleable in the popup.
- It cannot stop AiTM token theft after credentials are entered — the auth is genuinely completed on Microsoft's servers. The real prevention lives in the identity layer (phishing-resistant MFA, token protection, CAE, restricting device-code and user consent). This extension is the early-warning layer.
- Firefox builds from the same source (
npm run build:firefox); the detection logic is shared, only the manifest differs.