feat: expand confirm-tx warnings — closes #114 #118
@@ -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
|
||||
approval, or signature, the approval popup displays a prominent red warning
|
||||
banner alerting the user. The domain checker matches exact hostnames and all
|
||||
parent domains (subdomain matching), with whitelist overrides for legitimate
|
||||
sites that share a parent domain with a blocklisted entry.
|
||||
parent domains (subdomain matching).
|
||||
|
||||
#### Transaction Decoding
|
||||
|
||||
|
||||
@@ -21,17 +21,13 @@ const REFRESH_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
||||
const DELTA_STORAGE_KEY = "phishing-delta";
|
||||
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(
|
||||
(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 deltaWhitelist = new Set();
|
||||
let lastFetchTime = 0;
|
||||
let fetchPromise = null;
|
||||
let refreshTimer = null;
|
||||
@@ -50,11 +46,6 @@ function loadDeltaFromStorage() {
|
||||
data.blacklist.map((d) => d.toLowerCase()),
|
||||
);
|
||||
}
|
||||
if (data.whitelist && Array.isArray(data.whitelist)) {
|
||||
deltaWhitelist = new Set(
|
||||
data.whitelist.map((d) => d.toLowerCase()),
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// localStorage unavailable or corrupt — start empty
|
||||
}
|
||||
@@ -67,7 +58,6 @@ function saveDeltaToStorage() {
|
||||
try {
|
||||
const data = {
|
||||
blacklist: Array.from(deltaBlacklist),
|
||||
whitelist: Array.from(deltaWhitelist),
|
||||
};
|
||||
const json = JSON.stringify(data);
|
||||
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.
|
||||
* Used for both live fetches and testing.
|
||||
*
|
||||
* @param {{ blacklist?: string[], whitelist?: string[] }} config
|
||||
* @param {{ blacklist?: string[] }} config
|
||||
*/
|
||||
function loadConfig(config) {
|
||||
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
|
||||
deltaBlacklist = new Set(
|
||||
liveBlacklist.filter((d) => !vendoredBlacklist.has(d)),
|
||||
);
|
||||
deltaWhitelist = new Set(
|
||||
liveWhitelist.filter((d) => !vendoredWhitelist.has(d)),
|
||||
);
|
||||
|
||||
lastFetchTime = Date.now();
|
||||
saveDeltaToStorage();
|
||||
@@ -124,7 +110,6 @@ function hostnameVariants(hostname) {
|
||||
/**
|
||||
* Check if a hostname is on the phishing blocklist.
|
||||
* Checks delta first (fresh/recent scam sites), then vendored list.
|
||||
* Whitelisted domains (delta + vendored) are never flagged.
|
||||
*
|
||||
* @param {string} hostname - The hostname to check.
|
||||
* @returns {boolean}
|
||||
@@ -133,11 +118,6 @@ function isPhishingDomain(hostname) {
|
||||
if (!hostname) return false;
|
||||
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
|
||||
for (const v of variants) {
|
||||
if (deltaBlacklist.has(v) || vendoredBlacklist.has(v)) return true;
|
||||
@@ -209,7 +189,6 @@ function getDeltaSize() {
|
||||
*/
|
||||
function _reset() {
|
||||
deltaBlacklist = new Set();
|
||||
deltaWhitelist = new Set();
|
||||
lastFetchTime = 0;
|
||||
fetchPromise = null;
|
||||
if (refreshTimer) {
|
||||
@@ -232,7 +211,5 @@ module.exports = {
|
||||
_reset,
|
||||
// Exposed for testing only
|
||||
_getVendoredBlacklistSize: () => vendoredBlacklist.size,
|
||||
_getVendoredWhitelistSize: () => vendoredWhitelist.size,
|
||||
_getDeltaBlacklist: () => deltaBlacklist,
|
||||
_getDeltaWhitelist: () => deltaWhitelist,
|
||||
};
|
||||
|
||||
@@ -23,9 +23,7 @@ const {
|
||||
hostnameVariants,
|
||||
_reset,
|
||||
_getVendoredBlacklistSize,
|
||||
_getVendoredWhitelistSize,
|
||||
_getDeltaBlacklist,
|
||||
_getDeltaWhitelist,
|
||||
} = require("../src/shared/phishingDomains");
|
||||
|
||||
// Reset delta state before each test to avoid cross-test contamination.
|
||||
@@ -45,21 +43,12 @@ describe("phishingDomains", () => {
|
||||
expect(_getVendoredBlacklistSize()).toBeGreaterThan(100000);
|
||||
});
|
||||
|
||||
test("vendored whitelist is loaded from bundled JSON", () => {
|
||||
expect(_getVendoredWhitelistSize()).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("detects domains from vendored blacklist", () => {
|
||||
// These are well-known phishing domains in the vendored list
|
||||
expect(isPhishingDomain("hopprotocol.pro")).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", () => {
|
||||
expect(getBlocklistSize()).toBeGreaterThan(100000);
|
||||
});
|
||||
@@ -99,7 +88,6 @@ describe("phishingDomains", () => {
|
||||
"brand-new-scam-site-xyz123.com",
|
||||
"hopprotocol.pro", // already in vendored
|
||||
],
|
||||
whitelist: [],
|
||||
});
|
||||
// Only the new domain should be in the delta
|
||||
expect(
|
||||
@@ -109,30 +97,14 @@ describe("phishingDomains", () => {
|
||||
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", () => {
|
||||
loadConfig({
|
||||
blacklist: ["first-scam-xyz.com"],
|
||||
whitelist: [],
|
||||
});
|
||||
expect(isPhishingDomain("first-scam-xyz.com")).toBe(true);
|
||||
|
||||
loadConfig({
|
||||
blacklist: ["second-scam-xyz.com"],
|
||||
whitelist: [],
|
||||
});
|
||||
expect(isPhishingDomain("first-scam-xyz.com")).toBe(false);
|
||||
expect(isPhishingDomain("second-scam-xyz.com")).toBe(true);
|
||||
@@ -142,7 +114,6 @@ describe("phishingDomains", () => {
|
||||
const baseSize = getBlocklistSize();
|
||||
loadConfig({
|
||||
blacklist: ["delta-only-scam-xyz.com"],
|
||||
whitelist: [],
|
||||
});
|
||||
expect(getBlocklistSize()).toBe(baseSize + 1);
|
||||
});
|
||||
@@ -152,7 +123,6 @@ describe("phishingDomains", () => {
|
||||
test("detects domain from delta blacklist", () => {
|
||||
loadConfig({
|
||||
blacklist: ["fresh-scam-xyz.com"],
|
||||
whitelist: [],
|
||||
});
|
||||
expect(isPhishingDomain("fresh-scam-xyz.com")).toBe(true);
|
||||
});
|
||||
@@ -174,34 +144,13 @@ describe("phishingDomains", () => {
|
||||
test("detects subdomain of blacklisted domain (delta)", () => {
|
||||
loadConfig({
|
||||
blacklist: ["delta-phish-xyz.com"],
|
||||
whitelist: [],
|
||||
});
|
||||
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", () => {
|
||||
loadConfig({
|
||||
blacklist: ["Delta-Scam-XYZ.COM"],
|
||||
whitelist: [],
|
||||
});
|
||||
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);
|
||||
});
|
||||
|
||||
test("handles config with no blacklist/whitelist keys", () => {
|
||||
test("handles config with no blacklist key", () => {
|
||||
loadConfig({});
|
||||
expect(getDeltaSize()).toBe(0);
|
||||
// Vendored list still works
|
||||
@@ -224,19 +173,16 @@ describe("phishingDomains", () => {
|
||||
test("saveDeltaToStorage persists delta under 256KiB", () => {
|
||||
loadConfig({
|
||||
blacklist: ["persisted-scam-xyz.com"],
|
||||
whitelist: ["persisted-safe-xyz.com"],
|
||||
});
|
||||
const stored = localStorage.getItem("phishing-delta");
|
||||
expect(stored).not.toBeNull();
|
||||
const data = JSON.parse(stored);
|
||||
expect(data.blacklist).toContain("persisted-scam-xyz.com");
|
||||
expect(data.whitelist).toContain("persisted-safe-xyz.com");
|
||||
});
|
||||
|
||||
test("delta is cleared on _reset", () => {
|
||||
loadConfig({
|
||||
blacklist: ["temp-scam-xyz.com"],
|
||||
whitelist: [],
|
||||
});
|
||||
expect(getDeltaSize()).toBe(1);
|
||||
_reset();
|
||||
@@ -251,7 +197,7 @@ describe("phishingDomains", () => {
|
||||
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("etherscan.io")).toBe(false);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user