feat: expand confirm-tx warnings — closes #114 #118

Merged
sneak merged 6 commits from fix/issue-114-expanded-warnings into main 2026-03-01 19:34:56 +01:00
3 changed files with 6 additions and 84 deletions
Showing only changes of commit 06324158aa - Show all commits

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

View File

@@ -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,
};

View File

@@ -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);
});