feat(extension): block phishing, malware and scam sites - #20
Merged
Conversation
Adds scam protection to the Shield: known-bad domains are blocked, and a top-level navigation to one lands on a warning page naming the site instead of a bare network error. The list. Licence was the binding constraint — most well-known blocklists cannot ship inside a commercial extension (hagezi and AdGuard's are GPL-3.0-only, Cloudflare Radar is CC BY-NC, OpenPhish's community feed is non-commercial, abuse.ch now needs an auth key). Three sources clear it: CERT-PL's Warning List, whose API spec grants use "without restriction, by all entities, manually or automatically", plus durablenapkin/scamblocklist and Phishing.Database (both MIT) for coverage outside Poland. 17,990 domains after collapsing subdomains a listed parent already covers. Two rules, not 18,000. `requestDomains` takes a whole array and matches subdomains implicitly, so a chunk of 5,000 domains costs one block rule plus one redirect rule. That matters because the ~30k *static* budget is already fully spent by the ads and privacy rulesets — this would not have fit. uBlock Origin Lite ships the same shape; its urlhaus ruleset is literally one rule holding 5,312 domains. Dynamic rules, not a static ruleset. Phishing domains have a median lifetime of hours, while a static ruleset only changes when a new version clears store review. Dynamic rules persist across restarts and updates and refresh on a 6-hour alarm; the bundled seed installs on first run so protection is never absent while waiting on a fetch, and a refresh where every feed fails keeps what is already installed rather than clearing it. The warning page gets the blocked URL from the redirect's `regexSubstitution` (`#\0` is the whole match), so it can name the site. That value comes from a hostile page, so it is validated to http(s) before use and set with textContent — a javascript: or data: fragment is refused outright, and "proceed anyway" is disabled when no hostname parses. The fragment never leaves the browser: nothing about browsing is sent anywhere, which is both a privacy position and a store-policy one. Also fixes a latent collision this feature would have hit: applyAllowlist removed *every* dynamic rule, so allowlisting a site for ads would have deleted the user's phishing protection. Dynamic ids are now banded per owner (src/background/rule-ids.js), and priorities are ordered so security blocking outranks the ad allowlist — turning off ad blocking on a site no longer turns off scam protection — while a bypass the user granted from the warning page outranks both. Incidentally makes the suite exit 0. Four storage reads indexed a possibly undefined result; from a floating promise at module load that surfaced as an unhandled rejection and failed the run whatever the assertions said. That was the pre-existing "2 errors" — 744 tests, exit 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q3vvUS9Q7m4ASyESCL2C8D
ralyodio
force-pushed
the
scam-protection
branch
from
August 29, 2026 08:50
cd59010 to
9c796ef
Compare
| }); | ||
| // The exemption is a dynamic rule; give the browser a moment to install | ||
| // it before navigating back into the site, or the redirect fires again. | ||
| setTimeout(() => window.location.replace(blockedUrl), 300); |
ThreatCrush Security Scan12 finding(s) MEDIUM: 8 | LOW: 4
Snippets are redacted; ThreatCrush never prints matched credential material. |
ralyodio
added a commit
that referenced
this pull request
Aug 29, 2026
Resolves the alarm-handler conflict in the background service worker: #20 added the security-list refresh alarm in the same spot phase 2 adds the vault auto-lock alarm. Both handlers are kept, each with its own early return. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fc77feRQws35YGu9BADW1a
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.
This was written for #17 but missed the merge
#17 was squash-merged at 07:20:45; this commit was pushed at ~07:37. It never reached master, so the scam protection described in #17 is not currently shipped. Re-opening it on its own branch.
The two commits already in master (the blocked-request view and the security-job fixes) are shared history and collapse away on merge — only the scam-protection commit is new.
What it does
Blocks known phishing, malware and scam domains, and sends a top-level navigation to one to a warning page naming the site instead of a bare network error. Toggle on the Shield panel, on by default.
The list
Licence was the binding constraint — most well-known blocklists cannot ship inside a commercial extension (hagezi and AdGuard's are GPL-3.0-only, Cloudflare Radar is CC BY-NC, OpenPhish's community feed is non-commercial, abuse.ch needs an auth key). Three sources clear it:
NEW-today)17,990 after collapsing subdomains a listed parent already covers. Refresh with
node scripts/update-phishing-list.js.Two rules, not 18,000
requestDomainstakes a whole array and matches subdomains implicitly, so a 5,000-domain chunk costs oneblockrule plus onemain_frameredirectrule. This matters: the ~30k static budget is already fully spent by the ads and privacy rulesets, so a per-domain ruleset would not have fit at all. I verified uBlock Origin Lite's shipped JSON uses exactly this shape — its urlhaus ruleset is literally one rule holding 5,312 domains.Dynamic rules, not a static ruleset: phishing domains have a median lifetime of hours; a static ruleset only changes when a new version clears store review. Bundled seed installs on first run, then a 6-hour refresh alarm. A refresh where every feed fails keeps what is installed rather than clearing it.
The warning page
Gets the blocked URL from the redirect's
regexSubstitution(#\0is the whole match). That value comes from a hostile page, so it is validated to http(s) before use and set withtextContent; ajavascript:ordata:fragment is refused, and "proceed anyway" is disabled when no hostname parses. The fragment never leaves the browser — nothing about browsing is sent anywhere, which is a store-policy position as much as a privacy one (Avast Online Security was de-listed in 2019 for sending URLs).A collision this would have hit
applyAllowlistremoved every dynamic rule — so allowlisting a site for ads would have silently deleted the user's phishing protection. Dynamic ids are now banded per owner (src/background/rule-ids.js) and priorities ordered so security blocking outranks the ad allowlist, while a user-granted bypass outranks both. Covered by a test that runs the real allowlist path and asserts the security rules survive.Also fixes the suite exiting non-zero
Four storage reads indexed a possibly-undefined result; from a floating promise at module load that surfaced as an unhandled rejection and failed the run regardless of assertions. This is the fix for the 2 errors currently on master, which is why #19 still reports them.
New permissions
https://hole.cert.pl/*andhttps://raw.githubusercontent.com/*(list downloads only), plusblocked.htmlas a web-accessible resource so it can be a redirect target.Testing
744 passing, suite exits 0. Chrome and Firefox both build clean, with
blocked.htmlat the package root and the 17,990-domain seed inrules/.Worth one manual check before release
That
requestDomains: ["example.com"]also matchessub.example.comis documented only by example. uBOL depends on it and the collapsing step assumes it, so it is near-certain — but if it were wrong the list would under-block, so it deserves one smoke test in a real browser.🤖 Generated with Claude Code
https://claude.ai/code/session_01Q3vvUS9Q7m4ASyESCL2C8D