- Add etherscanLabels module: scrapes Etherscan address pages for phishing/scam labels (Fake_Phishing*, Exploiter, scam warnings). Integrated as best-effort async check in addressWarnings. - Add phishingDomains module: fetches MetaMask's eth-phishing-detect blocklist (~231K domains) at runtime, caches in memory, refreshes every 24h. Checks hostnames with subdomain matching and whitelist overrides. - Integrate domain phishing checks into all approval flows: connection requests, transaction approvals, and signature requests show a prominent red warning banner when the requesting site is on the MetaMask blocklist. - Add unit tests for both modules (12 tests for etherscanLabels parsing, 15 tests for phishingDomains matching). Closes #114
167 lines
5.7 KiB
JavaScript
167 lines
5.7 KiB
JavaScript
const {
|
|
isPhishingDomain,
|
|
loadConfig,
|
|
getBlocklistSize,
|
|
hostnameVariants,
|
|
_reset,
|
|
} = require("../src/shared/phishingDomains");
|
|
|
|
// Reset state before each test to avoid cross-test contamination.
|
|
beforeEach(() => {
|
|
_reset();
|
|
});
|
|
|
|
describe("phishingDomains", () => {
|
|
describe("hostnameVariants", () => {
|
|
test("returns exact hostname plus parent domains", () => {
|
|
const variants = hostnameVariants("sub.evil.com");
|
|
expect(variants).toEqual(["sub.evil.com", "evil.com"]);
|
|
});
|
|
|
|
test("returns just the hostname for a bare domain", () => {
|
|
const variants = hostnameVariants("example.com");
|
|
expect(variants).toEqual(["example.com"]);
|
|
});
|
|
|
|
test("handles deep subdomain chains", () => {
|
|
const variants = hostnameVariants("a.b.c.d.com");
|
|
expect(variants).toEqual([
|
|
"a.b.c.d.com",
|
|
"b.c.d.com",
|
|
"c.d.com",
|
|
"d.com",
|
|
]);
|
|
});
|
|
|
|
test("lowercases hostnames", () => {
|
|
const variants = hostnameVariants("Evil.COM");
|
|
expect(variants).toEqual(["evil.com"]);
|
|
});
|
|
});
|
|
|
|
describe("loadConfig + isPhishingDomain", () => {
|
|
test("detects exact blacklisted domain", () => {
|
|
loadConfig({
|
|
blacklist: ["evil-phishing.com", "scam-swap.xyz"],
|
|
whitelist: [],
|
|
});
|
|
expect(isPhishingDomain("evil-phishing.com")).toBe(true);
|
|
expect(isPhishingDomain("scam-swap.xyz")).toBe(true);
|
|
});
|
|
|
|
test("returns false for clean domains", () => {
|
|
loadConfig({
|
|
blacklist: ["evil-phishing.com"],
|
|
whitelist: [],
|
|
});
|
|
expect(isPhishingDomain("etherscan.io")).toBe(false);
|
|
expect(isPhishingDomain("uniswap.org")).toBe(false);
|
|
});
|
|
|
|
test("detects subdomain of blacklisted domain", () => {
|
|
loadConfig({
|
|
blacklist: ["evil-phishing.com"],
|
|
whitelist: [],
|
|
});
|
|
expect(isPhishingDomain("app.evil-phishing.com")).toBe(true);
|
|
expect(isPhishingDomain("sub.app.evil-phishing.com")).toBe(true);
|
|
});
|
|
|
|
test("whitelist overrides blacklist", () => {
|
|
loadConfig({
|
|
blacklist: ["metamask.io"],
|
|
whitelist: ["metamask.io"],
|
|
});
|
|
expect(isPhishingDomain("metamask.io")).toBe(false);
|
|
});
|
|
|
|
test("whitelist on parent domain overrides blacklist", () => {
|
|
loadConfig({
|
|
blacklist: ["sub.legit.com"],
|
|
whitelist: ["legit.com"],
|
|
});
|
|
expect(isPhishingDomain("sub.legit.com")).toBe(false);
|
|
});
|
|
|
|
test("case-insensitive matching", () => {
|
|
loadConfig({
|
|
blacklist: ["Evil-Phishing.COM"],
|
|
whitelist: [],
|
|
});
|
|
expect(isPhishingDomain("evil-phishing.com")).toBe(true);
|
|
expect(isPhishingDomain("EVIL-PHISHING.COM")).toBe(true);
|
|
});
|
|
|
|
test("returns false for empty/null hostname", () => {
|
|
loadConfig({
|
|
blacklist: ["evil.com"],
|
|
whitelist: [],
|
|
});
|
|
expect(isPhishingDomain("")).toBe(false);
|
|
expect(isPhishingDomain(null)).toBe(false);
|
|
});
|
|
|
|
test("getBlocklistSize reflects loaded config", () => {
|
|
loadConfig({
|
|
blacklist: ["a.com", "b.com", "c.com"],
|
|
whitelist: ["d.com"],
|
|
});
|
|
expect(getBlocklistSize()).toBe(3);
|
|
});
|
|
|
|
test("handles config with no blacklist/whitelist keys", () => {
|
|
loadConfig({});
|
|
expect(isPhishingDomain("anything.com")).toBe(false);
|
|
expect(getBlocklistSize()).toBe(0);
|
|
});
|
|
|
|
test("re-loading config replaces previous data", () => {
|
|
loadConfig({
|
|
blacklist: ["old-scam.com"],
|
|
whitelist: [],
|
|
});
|
|
expect(isPhishingDomain("old-scam.com")).toBe(true);
|
|
|
|
loadConfig({
|
|
blacklist: ["new-scam.com"],
|
|
whitelist: [],
|
|
});
|
|
expect(isPhishingDomain("old-scam.com")).toBe(false);
|
|
expect(isPhishingDomain("new-scam.com")).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("real-world MetaMask blocklist patterns", () => {
|
|
test("detects known phishing domains from MetaMask list", () => {
|
|
loadConfig({
|
|
blacklist: [
|
|
"uniswap-trade.web.app",
|
|
"hopprotocol.pro",
|
|
"blast-pools.pages.dev",
|
|
],
|
|
whitelist: [],
|
|
});
|
|
expect(isPhishingDomain("uniswap-trade.web.app")).toBe(true);
|
|
expect(isPhishingDomain("hopprotocol.pro")).toBe(true);
|
|
expect(isPhishingDomain("blast-pools.pages.dev")).toBe(true);
|
|
});
|
|
|
|
test("does not flag legitimate domains whitelisted by MetaMask", () => {
|
|
loadConfig({
|
|
blacklist: ["opensea.pro"],
|
|
whitelist: [
|
|
"opensea.io",
|
|
"metamask.io",
|
|
"etherscan.io",
|
|
"opensea.pro",
|
|
],
|
|
});
|
|
expect(isPhishingDomain("opensea.io")).toBe(false);
|
|
expect(isPhishingDomain("metamask.io")).toBe(false);
|
|
expect(isPhishingDomain("etherscan.io")).toBe(false);
|
|
// opensea.pro is both blacklisted and whitelisted — whitelist wins
|
|
expect(isPhishingDomain("opensea.pro")).toBe(false);
|
|
});
|
|
});
|
|
});
|