All checks were successful
check / check (push) Successful in 13s
The MetaMask blocklist contains 2 entries with '*.' wildcard prefixes (e.g. *.coinbase-563513.com). These were stored literally and never matched because hostnameVariants() doesn't generate '*.' prefixed strings. Fix: normalizeDomain() strips the '*.' prefix at load time and during delta computation. The subdomain matching in hostnameVariants() already handles child domains correctly. Found during review.
248 lines
8.9 KiB
JavaScript
248 lines
8.9 KiB
JavaScript
const {
|
|
isPhishingDomain,
|
|
loadConfig,
|
|
getBlocklistSize,
|
|
getDeltaSize,
|
|
hostnameVariants,
|
|
binarySearch,
|
|
normalizeDomain,
|
|
_reset,
|
|
} = require("../src/shared/phishingDomains");
|
|
|
|
// The vendored baseline is loaded automatically via require().
|
|
// _reset() clears only the delta state, not the vendored baseline.
|
|
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("binarySearch", () => {
|
|
const sorted = ["alpha.com", "beta.com", "gamma.com", "zeta.com"];
|
|
|
|
test("finds existing elements", () => {
|
|
expect(binarySearch(sorted, "alpha.com")).toBe(true);
|
|
expect(binarySearch(sorted, "gamma.com")).toBe(true);
|
|
expect(binarySearch(sorted, "zeta.com")).toBe(true);
|
|
});
|
|
|
|
test("returns false for missing elements", () => {
|
|
expect(binarySearch(sorted, "aaa.com")).toBe(false);
|
|
expect(binarySearch(sorted, "delta.com")).toBe(false);
|
|
expect(binarySearch(sorted, "zzz.com")).toBe(false);
|
|
});
|
|
|
|
test("handles empty array", () => {
|
|
expect(binarySearch([], "anything")).toBe(false);
|
|
});
|
|
|
|
test("handles single-element array", () => {
|
|
expect(binarySearch(["only.com"], "only.com")).toBe(true);
|
|
expect(binarySearch(["only.com"], "other.com")).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("normalizeDomain", () => {
|
|
test("strips *. wildcard prefix", () => {
|
|
expect(normalizeDomain("*.evil.com")).toBe("evil.com");
|
|
expect(normalizeDomain("*.sub.evil.com")).toBe("sub.evil.com");
|
|
});
|
|
|
|
test("lowercases domains", () => {
|
|
expect(normalizeDomain("Evil.COM")).toBe("evil.com");
|
|
expect(normalizeDomain("*.Evil.COM")).toBe("evil.com");
|
|
});
|
|
|
|
test("passes through normal domains unchanged", () => {
|
|
expect(normalizeDomain("example.com")).toBe("example.com");
|
|
});
|
|
});
|
|
|
|
describe("wildcard domain handling", () => {
|
|
test("wildcard blacklist entries match via loadConfig", () => {
|
|
loadConfig({
|
|
blacklist: ["*.scam-site.com", "normal-scam.com"],
|
|
whitelist: [],
|
|
});
|
|
// *.scam-site.com is normalized to scam-site.com
|
|
expect(isPhishingDomain("scam-site.com")).toBe(true);
|
|
expect(isPhishingDomain("sub.scam-site.com")).toBe(true);
|
|
expect(isPhishingDomain("normal-scam.com")).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("vendored baseline detection", () => {
|
|
// These tests verify that the vendored phishing-domains.json
|
|
// is loaded and searchable without any delta loaded.
|
|
|
|
test("getBlocklistSize reflects vendored list (no delta)", () => {
|
|
// The vendored list has 231k+ domains; delta is empty after reset.
|
|
expect(getBlocklistSize()).toBeGreaterThan(200000);
|
|
expect(getDeltaSize()).toBe(0);
|
|
});
|
|
|
|
test("returns false for clean domains against vendored list", () => {
|
|
expect(isPhishingDomain("google.com")).toBe(false);
|
|
expect(isPhishingDomain("github.com")).toBe(false);
|
|
});
|
|
|
|
test("returns false for empty/null hostname", () => {
|
|
expect(isPhishingDomain("")).toBe(false);
|
|
expect(isPhishingDomain(null)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("delta (loadConfig) + isPhishingDomain", () => {
|
|
test("detects domains loaded into delta via loadConfig", () => {
|
|
loadConfig({
|
|
blacklist: ["evil-phishing.com", "scam-swap.xyz"],
|
|
whitelist: [],
|
|
});
|
|
expect(isPhishingDomain("evil-phishing.com")).toBe(true);
|
|
expect(isPhishingDomain("scam-swap.xyz")).toBe(true);
|
|
});
|
|
|
|
test("detects subdomain of delta-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("delta whitelist overrides delta blacklist", () => {
|
|
loadConfig({
|
|
blacklist: ["metamask.io"],
|
|
whitelist: ["metamask.io"],
|
|
});
|
|
expect(isPhishingDomain("metamask.io")).toBe(false);
|
|
});
|
|
|
|
test("delta 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 in delta", () => {
|
|
loadConfig({
|
|
blacklist: ["Evil-Phishing.COM"],
|
|
whitelist: [],
|
|
});
|
|
expect(isPhishingDomain("evil-phishing.com")).toBe(true);
|
|
expect(isPhishingDomain("EVIL-PHISHING.COM")).toBe(true);
|
|
});
|
|
|
|
test("getDeltaSize reflects loaded delta", () => {
|
|
loadConfig({
|
|
blacklist: ["a.com", "b.com", "c.com"],
|
|
whitelist: ["d.com"],
|
|
});
|
|
expect(getDeltaSize()).toBe(3);
|
|
});
|
|
|
|
test("re-loading config replaces previous delta", () => {
|
|
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);
|
|
});
|
|
|
|
test("handles config with no blacklist/whitelist keys", () => {
|
|
loadConfig({});
|
|
expect(getDeltaSize()).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe("real-world MetaMask blocklist patterns (via delta)", () => {
|
|
test("detects known phishing domains loaded as delta", () => {
|
|
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("delta whitelist overrides vendored blacklist entries", () => {
|
|
// If a domain is in the vendored blacklist but a fresh whitelist
|
|
// update adds it, the whitelist should win.
|
|
loadConfig({
|
|
blacklist: [],
|
|
whitelist: ["opensea.io", "metamask.io", "etherscan.io"],
|
|
});
|
|
expect(isPhishingDomain("opensea.io")).toBe(false);
|
|
expect(isPhishingDomain("metamask.io")).toBe(false);
|
|
expect(isPhishingDomain("etherscan.io")).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("delta + vendored interaction", () => {
|
|
test("delta blacklist entries are found even with empty vendored match", () => {
|
|
// This domain is (almost certainly) not in the vendored list
|
|
const uniqueDomain =
|
|
"test-unique-domain-not-in-vendored-" +
|
|
Date.now() +
|
|
".example.com";
|
|
expect(isPhishingDomain(uniqueDomain)).toBe(false);
|
|
|
|
loadConfig({
|
|
blacklist: [uniqueDomain],
|
|
whitelist: [],
|
|
});
|
|
expect(isPhishingDomain(uniqueDomain)).toBe(true);
|
|
});
|
|
|
|
test("getBlocklistSize includes both vendored and delta", () => {
|
|
const baseSize = getBlocklistSize();
|
|
loadConfig({
|
|
blacklist: ["new-a.com", "new-b.com"],
|
|
whitelist: [],
|
|
});
|
|
expect(getBlocklistSize()).toBe(baseSize + 2);
|
|
});
|
|
});
|
|
});
|