Skip to content

Subnet345LLC/FixClick

Repository files navigation

FixClick

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 loginfmt username field) from a non-Microsoft origin (reverse-proxy kits such as Evilginx / EvilProxy). Keying on loginfmt avoids 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.

Design goals

  • 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/ + one register() line + one manifest entry.

Architecture

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 uses background.scripts + browser_specific_settings.gecko). The content-script js lists are declared once in base, so there is nothing to keep in sync. The code uses chrome.* with callbacks, which Firefox aliases — so no webextension-polyfill is needed. world: "MAIN" content scripts require Chrome 111+ / Firefox 128+; the Firefox build targets 140+ (current ESR) for the data_collection_permissions key.

Two content-script worlds:

  • MAIN world (main-world-hook.js) runs at document_start and wraps navigator.clipboard.writeText, document.execCommand('copy'), and DataTransfer.setData. It relays clipboard writes to the isolated world via window.postMessage — it contains no detection logic.
  • Isolated world (the ordered js list) shares one sandbox scope. Files attach to a single globalThis.FixClick namespace and load in dependency order, with content-orchestrator.js last as the entry point.

Build

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.

Load it (unpacked)

Chrome / Edge / any Chromium browser (requires Chromium 111+):

  1. npm run build:chrome
  2. Go to chrome://extensions (or edge://extensions).
  3. Enable Developer modeLoad unpacked → select dist/chrome.

Firefox (requires Firefox 140+):

  1. npm run build:firefox

  2. Go to about:debugging#/runtime/this-firefoxLoad Temporary Add-on → select dist/firefox/manifest.json. (Or, with the web-ext tool: 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.

Tests

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.

Verify detection (manual)

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 cloud loginfmt form 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.htmlno finding. It references the Microsoft auth CDN and shows branding + a "Sign in with Microsoft" button but renders no loginfmt field, so the tightened AiTM check correctly stays silent.

Open the toolbar popup to see recent detections and toggle detectors on/off.

Adding a new detector

  1. 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;
    })();
  2. Add it to the ISOLATED_JS list in manifests/manifest.config.mjs (before content-orchestrator.js) — one edit, applies to every target.
  3. registry.register(new NS.MyDetector()); in content-orchestrator.js.
  4. npm run build.

Limitations (by design, be honest about these)

  • 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.

About

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.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors