On-device scam & phishing protection for Chrome, Edge, Brave, Opera and Firefox.
Warns you before a page tricks you, spots brand look-alikes, checks suspicious messages — and nothing you browse, type or check ever leaves your device.
Install from the Chrome Web Store · Privacy policy · Changelog · Threat feed · Report a problem · Support
- Install
- What it does
- How it works
- Privacy
- Permissions
- Threat feed
- Optional community reporting (the relay)
- Project layout
- Development
- Models
- Releasing
- Upgrade safety
- Contributing · Security · License · Support the project
| Browser | How |
|---|---|
| Chrome, Edge, Brave, Opera, Vivaldi (any Chromium browser) | Install from the Chrome Web Store (extension ID fojjjofjimbfoddafoampojopijnlihl). |
| Firefox 128+ | Download scamshield-firefox.zip from the latest GitHub Release, unzip it, and load it via about:debugging#/runtime/this-firefox → Load Temporary Add-on → manifest.json (temporary add-ons are removed on restart). An addons.mozilla.org listing is pending; this row will link to it once live. |
| From source | chrome://extensions → Developer mode → Load unpacked → select this folder. See Development. |
Current release: 0.5.0 (submitted to the Chrome Web Store and awaiting review; the store serves 0.3.1 until it clears) — see the changelog for everything that changed since the 0.3.1 store version. The package is about 0.6 MB unpacked / ~170 KB zipped.
ScamShield watches every page you open and steps in only when something looks wrong. It is conservative by design: a page is only marked dangerous when at least two independent signals agree, and a single weak signal never produces more than a yellow suspicious notice.
Detection
- Phishing & scam warnings with plain-language reasons — a banner on suspicious/dangerous pages and a one-click Leave this page.
- Page analysis — an on-device model reads the page itself (wording, layout, login-form structure), catching brand-new phishing pages that a URL-only check would miss.
- Brand look-alike detection by icon — favicons and logos are perceptually hashed and matched against a 64-brand table (49 with icon hashes) including UAE banks, telcos and government services (Emirates NBD, ADCB, FAB, Mashreq, RAKBANK, e&, du, Noon, Aramex, Talabat, Careem, ADNOC, DEWA, ICP, MOHRE, Dubai Police, UAE PASS, Emirates, Etihad) plus PayPal, Microsoft, Google, Apple, DHL and more. A page using a brand's icon with a password form on the wrong domain is flagged even if the brand's name never appears.
- URL model + heuristics — a gradient-boosted URL classifier and hand-written rules (look-alike domains, punycode, suspicious tokens, IP hosts, deep-path tricks, brand-in-subdomain, …), with a built-in safe-domain allowlist for top sites.
- Known-bad domain blocking — a
declarativeNetRequestruleset refreshed daily from the open-source scamshield-feed (OpenPhish + URLhaus, heavily false-positive filtered).
Intervention
- Fake-login-form guard — intercepts a submit that would send your credentials to a foreign domain (including programmatic
form.submit()via a MAIN-world hook) and asks first. Single-sign-on logins via Google/Microsoft/Okta etc. are recognised as legitimate. - "Take me to the real site" — one-click rescue link on brand-impersonation pages.
- Crypto-wallet guard — warns before risky
window.ethereumrequests (blind signatures, unlimited approvals,setApprovalForAll, Permit2/Seaport grants) and flags recovery-phrase harvesting. - Clipboard-hijack guard — warns when a page copies a shell command or swaps a crypto address onto your clipboard ("paste this to verify" / ClickFix).
- Tech-support scare-page guard — throttles
alert/beforeunloadloops, detects "your PC is infected, call this number" text, and offers a one-click escape. - Hides "you won a prize" / giveaway scam content.
Tools & control
- Scam message checker — paste any SMS/WhatsApp/email text or link into the popup for an instant, fully private verdict.
- Trust a site for 1 hour, until tomorrow, or always; Report a mistake in one tap.
- Protection history and stats — a local-only log of what was blocked or flagged (hostnames and event types only).
- Redesigned popup and settings, dark mode, first-run onboarding, accessible warnings (
role=alert/role=dialog, Escape-to-cancel, focus management). - Re-scans single-page apps on route changes.
Everything runs inside the extension. There is no server-side analysis.
┌──────────────────────── on your device ─────────────────────────┐
page load / SPA route ─►│ engine/features.js URL features ─┐ │
│ engine/heuristics.js URL + DOM rules├─► engine/verdict.js │
│ engine/url_model.js URL model (GB) │ conservative fusion ├─► banner · popup ·
│ engine/page_features.js │ safe / suspicious / │ form guard ·
│ engine/page_model.js page model (LR)┤ dangerous + reasons │ rescue link
│ engine/image_hash.js icon dHash vs │ │
│ + brand_icons.json brand table ─┘ │
│ rules/blocklist.json declarativeNetRequest (known-bad hosts) │
└──────────────────────────────────────────────────────────────────┘
| Stage | Where | What it does |
|---|---|---|
| URL features & rules | engine/features.js, engine/heuristics.js, engine/constants.js |
Registrable-domain parsing (~150 multi-label public suffixes), brand/safe-domain tables, look-alike and token rules. |
| URL model | engine/url_model.js + model/url-model.json |
Gradient-boosted trees (scikit-learn HistGradientBoosting, exported to JSON, evaluated in pure JS). Holdout acc 0.988 / AUC 0.998. |
| Page-content model | engine/page_features.js, engine/page_model.js + model/page-content.json |
Logistic regression over 32,768 hashed tokens (FNV-1a) + 16 dense layout/form features. The same extractor runs in the browser and in the Node crawler, so there is no Python mirror to drift. |
| Icon matching | engine/image_hash.js, engine/brand_icons.json |
dHash of the page's favicon/logo compared to 49 brand hashes (Hamming ≤ 4), with an order-independent ambiguity guard so near-identical brand icons can't collide. Icons are fetched by the service worker without cookies and cached in storage.session. |
| Fusion | engine/verdict.js |
Content signals alone ≤ suspicious. Dangerous needs corroboration: a URL rule ≥ 0.3, the URL model ≥ 0.7 and at least one URL rule ≥ 0.15, or a visual (icon) brand match. |
| Intervention | content/content_script.js, content/actions.js, content/detectors/* |
Banner, form guard, wallet/clipboard/tech-scam guards (MAIN world), rescue link, scam-content hiding. |
| Background | background/service_worker.js |
Settings, trust list, history, feed updates (12 h alarm), icon hashing, report queue, per-tab verdict state. |
| UI | popup.*, options.*, onboarding.html, ui/* |
340 px popup with a 4-state status card, sidebar settings, onboarding, shared design tokens (ui/tokens.css), light/dark themes. |
The engine modules are DOM-free UMD wrappers, so the same files run in the browser, in node --test, and in the training crawler.
ScamShield collects nothing by default. All classification runs locally. Full policy: https://joelstephen97.github.io/scamshield/privacy.html (source: privacy.html / store/privacy-policy.md).
The extension makes exactly three kinds of network request:
| Request | When | What is sent | Can be turned off |
|---|---|---|---|
| Threat-feed download | On install, then every 12 h | A plain GET for a static JSON file from GitHub — the same file for every user. Nothing about you or your browsing. | Yes — clear the feed URL in Settings (or point it at your own feed). |
| Icon fetch | When a page references a favicon/logo that needs checking | A GET for the icon file the page itself references (usually same-site, sometimes the site's CDN), with no cookies or credentials. No third party is contacted. | It is part of page analysis, which can be turned off in Settings. |
| Community report (relay) | Only if you opt in ("Help make ScamShield smarter"), and then only for pages flagged dangerous (max once per site per day) or when you press Report a mistake | Hostname, verdict + reason codes, numeric risk signals (hashed word counts, form/input/link counts — never text), matched brand names, extension version, hour. Never the full URL, page text, anything you typed, cookies, or any identifier. | Off by default; turn off any time and queued reports are discarded. |
Everything else — settings, trusted sites, protection history (hostnames + event types only, 200-event ring buffer), queued reports while opted in — lives in the browser's extension storage, can be cleared in Settings, and is removed on uninstall.
If you report a mistake while reporting is off, ScamShield simply opens a pre-filled GitHub issue in a new tab; nothing is sent unless you submit it yourself.
ScamShield requests the minimum it needs and has not added a permission since 0.3.1 (adding one would disable the extension for existing users until re-approved).
| Permission | Why |
|---|---|
storage |
Settings, trusted sites, local history/stats, report queue. |
declarativeNetRequest |
Block known scam domains with a static ruleset — no reading or intercepting of your traffic. |
alarms |
The 12-hour feed refresh and the report-queue flush. |
Host permissions http://*/*, https://*/* |
Scams can be hosted anywhere, so the content script must be able to read the current page. Content scripts are statically declared; the scripting API is not used. |
No remote code, no WebAssembly, no web_accessible_resources. Reviewer-facing detail: store/permissions-justification.md.
The blocklist is built in a separate open-source repo, joelstephen97/scamshield-feed, and published as a static file:
https://raw.githubusercontent.com/joelstephen97/scamshield-feed/main/blocklist.json
- Sources: OpenPhish community feed + URLhaus online URLs, reduced to domain-level rules.
- False-positive guards: anything in the Tranco top-10k is dropped; on shared hosts (
pages.dev,netlify.app,github.io, …) only the exact abusive hostname is blocked; path-based gateways (IPFS, archive.org, Drive/Dropbox) are skipped; multi-label public suffixes can never become a rule; capped at 5,000 rules. - Schedule: rebuilt daily by GitHub Actions; the extension downloads it on install and every 12 hours, shows last updated and rule count in Settings, and ships a 501-rule snapshot in
rules/blocklist.jsonso blocking works before the first download. - Your choice: Settings lets you point the URL at your own feed (same
{ "version": n, "rules": ["||host^", …] }format) or clear it to disable downloads entirely.
Off by default. When a user turns on Help make ScamShield smarter, anonymised host-level reports go to a tiny relay so the models can be retrained on real misses and false positives.
browser (opt-in) Vercel project "scamshield-relay" Neon Postgres maintainer
──────────────── ──────────────────────────────── ───────────── ──────────
verdict ─► engine/report_payload ─► POST /api/report ───────────────────────► reports table ─┐
(host-level, ≤ 32 KB) validate · size-check · 60 req/h per IP (no IPs, URLs, │
queue ≤ 50, flush 12 h (IP kept only in memory) or page text) │
GET /api/export (Bearer EXPORT_TOKEN) ◄──────────────────┘─► model/pull_reports.py
GET /api/purge (Vercel Cron 04:00 UTC, rows > 180 d) └─► train_page.py / train.py
GET /api/health └─► next store release
- Code:
relay/(Node serverless functions, ownpackage.jsonand 25 tests; deployed with Vercel Root Directory =relay). Live athttps://scamshield-relay-seven.vercel.app(/api/health). - Data model:
relay/schema.sql—id, received_at, kind, label, host, reg_domain, level, score, ext_version, payload jsonb. Rows are deleted after 180 days. - Payload contract:
engine/report_payload.js(what the extension sends) andrelay/lib/validate.js(what the relay accepts). Anything outside the contract is rejected. - Security: constant-time bearer-token comparison,
content-lengthpre-check, platform-appended client IP used only as an in-memory rate-limit key, purge requires both the cron secret and Vercel'sx-vercel-cronheader. Details inrelay/README.md. - Retraining:
model/pull_reports.pypulls new rows into local training data (user-labelled rows are trusted; auto "dangerous" rows go to a review file first). New models ship inside the next store release — there is no over-the-air model update.
manifest.json / manifest.firefox.json MV3 manifests (Chromium / Firefox)
background/service_worker.js settings, trust, history, feed OTA, icon hashing, report queue
content/ content script, actions (banner/overlays), MAIN-world detectors
engine/ DOM-free detection engine (UMD; runs in browser + Node)
constants.js features.js heuristics.js verdict.js trust.js
url_model.js page_features.js page_model.js image_hash.js brand_icons.json
message_rules.js wallet_rules.js clipboard_rules.js techscam_rules.js report_payload.js
model/ training code + shipped model JSON (see model/README.md)
rules/blocklist.json bundled declarativeNetRequest snapshot (refreshed from the feed)
ui/ shared design tokens, theme, icons, formatting, onboarding script
popup.* options.* onboarding.html extension UI
privacy.html index.html GitHub Pages: privacy policy + project page
relay/ opt-in reporting relay (Vercel + Neon) — own package.json/tests
store/ store listing copy, permissions justification, screenshots, promo tiles
scripts/ build.js (zips), bundle-models.js, crawl-pages.js, gen-parity.js
tools/ screenshots.js, promo.js, build-brand-hashes.js, measure-icon-fp.js
tests/unit node --test suites (375 tests)
tests/e2e Playwright suites + fixture servers (HTTP :5599 / HTTPS :5600)
npm install
npx playwright install chromium
npm run test:unit # node --test, 375 tests
npm run test:e2e # Playwright, 34 tests (headed: HEADLESS=false npx playwright test)
npm test # both
npm run build # dist/scamshield-chrome.zip + dist/scamshield-firefox.zip (asserts size + manifest parity)Other scripts: npm run screenshots (regenerate store/screenshots/* via Playwright + tools/frame.html), npm run promo (promo tiles), npm run build:brands (rebuild engine/brand_icons.json from live brand sites), npm run measure:icon-fp (icon false-positive sweep), npm run gen:parity (200-URL JS↔Python parity set), npm run crawl:pages (page-model training crawl — feature rows only, no HTML stored), npm run bundle:models (wrap model JSON into the .js bundles the manifest loads).
E2E fixtures: tests/e2e/server.js serves phishing/benign fixture pages on HTTP :5599 and HTTPS :5600 (self-signed cert in tests/e2e/certs/); the Playwright config maps fixture hosts such as amazon.ae, accounts.google.com and secure-paypa1-login.com to 127.0.0.1 with --host-resolver-rules. To see a warning by hand: node tests/e2e/server.js, load the unpacked extension, open http://localhost:5599/phishing-login.html.
Relay: cd relay && npm install && npm test. Deploy notes in relay/README.md.
Conventions: vanilla JS, no bundler, no TypeScript; engine modules are UMD and DOM-free; every detector ships with unit tests; anything user-visible gets an e2e; commits use conventional prefixes (feat:, fix:, docs:, chore:). See CONTRIBUTING.md.
Both models are trained offline with scikit-learn, exported to JSON, and evaluated in pure JS (no runtime, no WASM). Working models are committed, so retraining is optional. Full details, metrics and caveats: model/README.md.
| Model | Training data | Shipped as | Last metrics |
|---|---|---|---|
| URL (HistGradientBoosting) | model/data/real.csv — OpenPhish + URLhaus positives, Tranco negatives expanded with realistic deep paths (build_dataset.py), ~22.5k rows |
model/url-model.json → model/url-model.js |
acc 0.988 · precision 0.993 · recall 0.983 · AUC 0.998; a deep-URL regression gate runs on every training run |
| Page content (logistic regression) | model/data/pages.jsonl from npm run crawl:pages — 2,917 rows (375 positive / 2,542 negative incl. 269 legitimate login pages), 1,915 registrable domains |
model/page-content.json → model/page-content.js |
grouped-holdout AUC 0.886; threshold 0.80 chosen for precision (precision 1.000, FPR 0.00 % on legit-login holdout, recall 0.107 — content alone can only raise suspicious) |
# Python ≥ 3.10, from the repo root
python -m venv model/.venv && model/.venv/Scripts/python -m pip install -r model/requirements.txt
model/.venv/Scripts/python model/train.py # URL model → url-model.json + url_parity.json
model/.venv/Scripts/python model/train_page.py # page model → page-content.json + page_parity.json
npm run bundle:models && npm run test:unit # parity tests assert JS == Python to 1e-4Known limits (honest): the page model is data-limited (few live positives on any given day) — recall will improve as opt-in reports accumulate; 15 of the 64 brands have no icon hash yet (npm run build:brands re-run); Firefox ICO decoding is unverified.
- Bump the version in three places:
manifest.json,manifest.firefox.json,package.json; updateCHANGELOG.mdand, if user-visible, thewhatsNewSeenversion inpopup.js. npm test && npm run build— the build asserts the zip stays small (≤ 2.5 MB) and that the staged manifest matches the source.- Regenerate store assets if the UI changed:
npm run screenshots && npm run promo. - Commit, tag (
git tag vX.Y.Z && git push origin refs/tags/vX.Y.Z), then publish a GitHub Release with both zips attached:gh release create vX.Y.Z dist/scamshield-chrome.zip dist/scamshield-firefox.zip --title "ScamShield X.Y.Z" --notes-file <notes>(the Firefox install instructions link to the latest release). - Upload
dist/scamshield-chrome.zipto the Chrome Web Store developer dashboard anddist/scamshield-firefox.zipto AMO, using the copy instore/chrome-listing.md/store/firefox-listing.mdand the checklist instore/submission-checklist.md.
Active users are on the store version, so every release must be a drop-in upgrade:
- No new permissions — manifest permissions are exactly
storage,declarativeNetRequest,alarms+ http/https host permissions (a unit test guards the built manifest). - Storage is additive — new settings get defaults on update; existing settings, trusted sites and history are never rewritten.
tests/e2e/upgrade.spec.jsboots the extension on a 0.3.1 storage fixture and checks nothing breaks. - Old placeholder URLs (feed
'', relay placeholder) are migrated to the current defaults; a user-customised URL is left alone.
Bug reports, false positives and missed scams are the most useful contributions — the easiest way is the in-extension Report a mistake button (opens a pre-filled issue when reporting is off) or open an issue. Pull requests welcome; please read CONTRIBUTING.md first (tests, the no-new-permissions rule, how to add a brand or a safe domain).
Please report vulnerabilities privately to jojostev@gmail.com rather than in a public issue — see SECURITY.md for scope (extension + relay) and what to expect.
ScamShield is free software, released under the GNU General Public License v3.0 or later — see LICENSE. You may use, study, modify and redistribute it under the same terms. The trained model files in model/ and the brand-icon hash table are covered by the same license. If you want to embed the detection engine in a product under different terms, get in touch.
Third-party data: the threat feed is built from the OpenPhish community feed and URLhaus (abuse.ch), each under their own terms; brand names and icons referenced for look-alike detection belong to their respective owners and are used solely to protect users from impersonation.
ScamShield is free, ad-free and on-device. If it helped you, consider supporting development via GitHub Sponsors or PayPal. Donations never change the privacy promise: nothing leaves your device.
Made by Joel Stephen · contact: jojostev@gmail.com