remove phishing domain whitelist support
All checks were successful
check / check (push) Successful in 13s

Remove all whitelist functionality from the phishing domain system.
The blocklist now only checks the blacklist — no whitelist overrides.

- Remove vendoredWhitelist and deltaWhitelist Sets
- Remove whitelist checks in isPhishingDomain()
- Remove whitelist from delta storage persistence
- Remove whitelist from loadConfig() delta computation
- Remove whitelist-specific test cases
- Update README to remove whitelist mention

Closes #114
This commit is contained in:
clawbot
2026-03-01 10:28:56 -08:00
committed by user
parent 293d781385
commit 06324158aa
3 changed files with 6 additions and 84 deletions

View File

@@ -803,8 +803,7 @@ small while ensuring fresh coverage of new phishing domains.
When a dApp on a blocklisted domain requests a wallet connection, transaction When a dApp on a blocklisted domain requests a wallet connection, transaction
approval, or signature, the approval popup displays a prominent red warning approval, or signature, the approval popup displays a prominent red warning
banner alerting the user. The domain checker matches exact hostnames and all banner alerting the user. The domain checker matches exact hostnames and all
parent domains (subdomain matching), with whitelist overrides for legitimate parent domains (subdomain matching).
sites that share a parent domain with a blocklisted entry.
#### Transaction Decoding #### Transaction Decoding

View File

@@ -21,17 +21,13 @@ const REFRESH_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
const DELTA_STORAGE_KEY = "phishing-delta"; const DELTA_STORAGE_KEY = "phishing-delta";
const MAX_DELTA_BYTES = 256 * 1024; // 256 KiB const MAX_DELTA_BYTES = 256 * 1024; // 256 KiB
// Vendored sets — built once from the bundled JSON. // Vendored set — built once from the bundled JSON.
const vendoredBlacklist = new Set( const vendoredBlacklist = new Set(
(vendoredConfig.blacklist || []).map((d) => d.toLowerCase()), (vendoredConfig.blacklist || []).map((d) => d.toLowerCase()),
); );
const vendoredWhitelist = new Set(
(vendoredConfig.whitelist || []).map((d) => d.toLowerCase()),
);
// Delta sets — only entries from live list that are NOT in vendored. // Delta set — only entries from live list that are NOT in vendored.
let deltaBlacklist = new Set(); let deltaBlacklist = new Set();
let deltaWhitelist = new Set();
let lastFetchTime = 0; let lastFetchTime = 0;
let fetchPromise = null; let fetchPromise = null;
let refreshTimer = null; let refreshTimer = null;
@@ -50,11 +46,6 @@ function loadDeltaFromStorage() {
data.blacklist.map((d) => d.toLowerCase()), data.blacklist.map((d) => d.toLowerCase()),
); );
} }
if (data.whitelist && Array.isArray(data.whitelist)) {
deltaWhitelist = new Set(
data.whitelist.map((d) => d.toLowerCase()),
);
}
} catch { } catch {
// localStorage unavailable or corrupt — start empty // localStorage unavailable or corrupt — start empty
} }
@@ -67,7 +58,6 @@ function saveDeltaToStorage() {
try { try {
const data = { const data = {
blacklist: Array.from(deltaBlacklist), blacklist: Array.from(deltaBlacklist),
whitelist: Array.from(deltaWhitelist),
}; };
const json = JSON.stringify(data); const json = JSON.stringify(data);
if (json.length < MAX_DELTA_BYTES) { if (json.length < MAX_DELTA_BYTES) {
@@ -85,19 +75,15 @@ function saveDeltaToStorage() {
* Load a pre-parsed config and compute the delta against the vendored list. * Load a pre-parsed config and compute the delta against the vendored list.
* Used for both live fetches and testing. * Used for both live fetches and testing.
* *
* @param {{ blacklist?: string[], whitelist?: string[] }} config * @param {{ blacklist?: string[] }} config
*/ */
function loadConfig(config) { function loadConfig(config) {
const liveBlacklist = (config.blacklist || []).map((d) => d.toLowerCase()); const liveBlacklist = (config.blacklist || []).map((d) => d.toLowerCase());
const liveWhitelist = (config.whitelist || []).map((d) => d.toLowerCase());
// Delta = entries in the live list that are NOT in the vendored list // Delta = entries in the live list that are NOT in the vendored list
deltaBlacklist = new Set( deltaBlacklist = new Set(
liveBlacklist.filter((d) => !vendoredBlacklist.has(d)), liveBlacklist.filter((d) => !vendoredBlacklist.has(d)),
); );
deltaWhitelist = new Set(
liveWhitelist.filter((d) => !vendoredWhitelist.has(d)),
);
lastFetchTime = Date.now(); lastFetchTime = Date.now();
saveDeltaToStorage(); saveDeltaToStorage();
@@ -124,7 +110,6 @@ function hostnameVariants(hostname) {
/** /**
* Check if a hostname is on the phishing blocklist. * Check if a hostname is on the phishing blocklist.
* Checks delta first (fresh/recent scam sites), then vendored list. * Checks delta first (fresh/recent scam sites), then vendored list.
* Whitelisted domains (delta + vendored) are never flagged.
* *
* @param {string} hostname - The hostname to check. * @param {string} hostname - The hostname to check.
* @returns {boolean} * @returns {boolean}
@@ -133,11 +118,6 @@ function isPhishingDomain(hostname) {
if (!hostname) return false; if (!hostname) return false;
const variants = hostnameVariants(hostname); const variants = hostnameVariants(hostname);
// Whitelist takes priority — check delta whitelist first, then vendored
for (const v of variants) {
if (deltaWhitelist.has(v) || vendoredWhitelist.has(v)) return false;
}
// Check delta blacklist first (fresh/recent scam sites), then vendored // Check delta blacklist first (fresh/recent scam sites), then vendored
for (const v of variants) { for (const v of variants) {
if (deltaBlacklist.has(v) || vendoredBlacklist.has(v)) return true; if (deltaBlacklist.has(v) || vendoredBlacklist.has(v)) return true;
@@ -209,7 +189,6 @@ function getDeltaSize() {
*/ */
function _reset() { function _reset() {
deltaBlacklist = new Set(); deltaBlacklist = new Set();
deltaWhitelist = new Set();
lastFetchTime = 0; lastFetchTime = 0;
fetchPromise = null; fetchPromise = null;
if (refreshTimer) { if (refreshTimer) {
@@ -232,7 +211,5 @@ module.exports = {
_reset, _reset,
// Exposed for testing only // Exposed for testing only
_getVendoredBlacklistSize: () => vendoredBlacklist.size, _getVendoredBlacklistSize: () => vendoredBlacklist.size,
_getVendoredWhitelistSize: () => vendoredWhitelist.size,
_getDeltaBlacklist: () => deltaBlacklist, _getDeltaBlacklist: () => deltaBlacklist,
_getDeltaWhitelist: () => deltaWhitelist,
}; };

View File

@@ -23,9 +23,7 @@ const {
hostnameVariants, hostnameVariants,
_reset, _reset,
_getVendoredBlacklistSize, _getVendoredBlacklistSize,
_getVendoredWhitelistSize,
_getDeltaBlacklist, _getDeltaBlacklist,
_getDeltaWhitelist,
} = require("../src/shared/phishingDomains"); } = require("../src/shared/phishingDomains");
// Reset delta state before each test to avoid cross-test contamination. // Reset delta state before each test to avoid cross-test contamination.
@@ -45,21 +43,12 @@ describe("phishingDomains", () => {
expect(_getVendoredBlacklistSize()).toBeGreaterThan(100000); expect(_getVendoredBlacklistSize()).toBeGreaterThan(100000);
}); });
test("vendored whitelist is loaded from bundled JSON", () => {
expect(_getVendoredWhitelistSize()).toBeGreaterThan(0);
});
test("detects domains from vendored blacklist", () => { test("detects domains from vendored blacklist", () => {
// These are well-known phishing domains in the vendored list // These are well-known phishing domains in the vendored list
expect(isPhishingDomain("hopprotocol.pro")).toBe(true); expect(isPhishingDomain("hopprotocol.pro")).toBe(true);
expect(isPhishingDomain("blast-pools.pages.dev")).toBe(true); expect(isPhishingDomain("blast-pools.pages.dev")).toBe(true);
}); });
test("vendored whitelist overrides vendored blacklist", () => {
// opensea.pro is whitelisted in the vendored config
expect(isPhishingDomain("opensea.pro")).toBe(false);
});
test("getBlocklistSize includes vendored entries", () => { test("getBlocklistSize includes vendored entries", () => {
expect(getBlocklistSize()).toBeGreaterThan(100000); expect(getBlocklistSize()).toBeGreaterThan(100000);
}); });
@@ -99,7 +88,6 @@ describe("phishingDomains", () => {
"brand-new-scam-site-xyz123.com", "brand-new-scam-site-xyz123.com",
"hopprotocol.pro", // already in vendored "hopprotocol.pro", // already in vendored
], ],
whitelist: [],
}); });
// Only the new domain should be in the delta // Only the new domain should be in the delta
expect( expect(
@@ -109,30 +97,14 @@ describe("phishingDomains", () => {
expect(getDeltaSize()).toBe(1); expect(getDeltaSize()).toBe(1);
}); });
test("delta whitelist entries are computed correctly", () => {
loadConfig({
blacklist: [],
whitelist: [
"new-safe-site-xyz789.com",
"opensea.pro", // already in vendored whitelist
],
});
expect(_getDeltaWhitelist().has("new-safe-site-xyz789.com")).toBe(
true,
);
expect(_getDeltaWhitelist().has("opensea.pro")).toBe(false);
});
test("re-loading config replaces previous delta", () => { test("re-loading config replaces previous delta", () => {
loadConfig({ loadConfig({
blacklist: ["first-scam-xyz.com"], blacklist: ["first-scam-xyz.com"],
whitelist: [],
}); });
expect(isPhishingDomain("first-scam-xyz.com")).toBe(true); expect(isPhishingDomain("first-scam-xyz.com")).toBe(true);
loadConfig({ loadConfig({
blacklist: ["second-scam-xyz.com"], blacklist: ["second-scam-xyz.com"],
whitelist: [],
}); });
expect(isPhishingDomain("first-scam-xyz.com")).toBe(false); expect(isPhishingDomain("first-scam-xyz.com")).toBe(false);
expect(isPhishingDomain("second-scam-xyz.com")).toBe(true); expect(isPhishingDomain("second-scam-xyz.com")).toBe(true);
@@ -142,7 +114,6 @@ describe("phishingDomains", () => {
const baseSize = getBlocklistSize(); const baseSize = getBlocklistSize();
loadConfig({ loadConfig({
blacklist: ["delta-only-scam-xyz.com"], blacklist: ["delta-only-scam-xyz.com"],
whitelist: [],
}); });
expect(getBlocklistSize()).toBe(baseSize + 1); expect(getBlocklistSize()).toBe(baseSize + 1);
}); });
@@ -152,7 +123,6 @@ describe("phishingDomains", () => {
test("detects domain from delta blacklist", () => { test("detects domain from delta blacklist", () => {
loadConfig({ loadConfig({
blacklist: ["fresh-scam-xyz.com"], blacklist: ["fresh-scam-xyz.com"],
whitelist: [],
}); });
expect(isPhishingDomain("fresh-scam-xyz.com")).toBe(true); expect(isPhishingDomain("fresh-scam-xyz.com")).toBe(true);
}); });
@@ -174,34 +144,13 @@ describe("phishingDomains", () => {
test("detects subdomain of blacklisted domain (delta)", () => { test("detects subdomain of blacklisted domain (delta)", () => {
loadConfig({ loadConfig({
blacklist: ["delta-phish-xyz.com"], blacklist: ["delta-phish-xyz.com"],
whitelist: [],
}); });
expect(isPhishingDomain("sub.delta-phish-xyz.com")).toBe(true); expect(isPhishingDomain("sub.delta-phish-xyz.com")).toBe(true);
}); });
test("delta whitelist overrides vendored blacklist", () => {
// hopprotocol.pro is in the vendored blacklist
expect(isPhishingDomain("hopprotocol.pro")).toBe(true);
loadConfig({
blacklist: [],
whitelist: ["hopprotocol.pro"],
});
// Now whitelisted via delta — should not be flagged
expect(isPhishingDomain("hopprotocol.pro")).toBe(false);
});
test("vendored whitelist overrides delta blacklist", () => {
loadConfig({
blacklist: ["opensea.pro"], // opensea.pro is vendored-whitelisted
whitelist: [],
});
expect(isPhishingDomain("opensea.pro")).toBe(false);
});
test("case-insensitive matching", () => { test("case-insensitive matching", () => {
loadConfig({ loadConfig({
blacklist: ["Delta-Scam-XYZ.COM"], blacklist: ["Delta-Scam-XYZ.COM"],
whitelist: [],
}); });
expect(isPhishingDomain("delta-scam-xyz.com")).toBe(true); expect(isPhishingDomain("delta-scam-xyz.com")).toBe(true);
expect(isPhishingDomain("DELTA-SCAM-XYZ.COM")).toBe(true); expect(isPhishingDomain("DELTA-SCAM-XYZ.COM")).toBe(true);
@@ -212,7 +161,7 @@ describe("phishingDomains", () => {
expect(isPhishingDomain(null)).toBe(false); expect(isPhishingDomain(null)).toBe(false);
}); });
test("handles config with no blacklist/whitelist keys", () => { test("handles config with no blacklist key", () => {
loadConfig({}); loadConfig({});
expect(getDeltaSize()).toBe(0); expect(getDeltaSize()).toBe(0);
// Vendored list still works // Vendored list still works
@@ -224,19 +173,16 @@ describe("phishingDomains", () => {
test("saveDeltaToStorage persists delta under 256KiB", () => { test("saveDeltaToStorage persists delta under 256KiB", () => {
loadConfig({ loadConfig({
blacklist: ["persisted-scam-xyz.com"], blacklist: ["persisted-scam-xyz.com"],
whitelist: ["persisted-safe-xyz.com"],
}); });
const stored = localStorage.getItem("phishing-delta"); const stored = localStorage.getItem("phishing-delta");
expect(stored).not.toBeNull(); expect(stored).not.toBeNull();
const data = JSON.parse(stored); const data = JSON.parse(stored);
expect(data.blacklist).toContain("persisted-scam-xyz.com"); expect(data.blacklist).toContain("persisted-scam-xyz.com");
expect(data.whitelist).toContain("persisted-safe-xyz.com");
}); });
test("delta is cleared on _reset", () => { test("delta is cleared on _reset", () => {
loadConfig({ loadConfig({
blacklist: ["temp-scam-xyz.com"], blacklist: ["temp-scam-xyz.com"],
whitelist: [],
}); });
expect(getDeltaSize()).toBe(1); expect(getDeltaSize()).toBe(1);
_reset(); _reset();
@@ -251,7 +197,7 @@ describe("phishingDomains", () => {
expect(isPhishingDomain("blast-pools.pages.dev")).toBe(true); expect(isPhishingDomain("blast-pools.pages.dev")).toBe(true);
}); });
test("does not flag legitimate whitelisted domains", () => { test("does not flag legitimate domains", () => {
expect(isPhishingDomain("opensea.io")).toBe(false); expect(isPhishingDomain("opensea.io")).toBe(false);
expect(isPhishingDomain("etherscan.io")).toBe(false); expect(isPhishingDomain("etherscan.io")).toBe(false);
}); });