All checks were successful
check / check (push) Successful in 5s
Expands the confirm-tx warning system with three new warning types, all using the existing `visibility:hidden/visible` pattern from PR #98 (no animations, no layout shift). ## Changes 1. **Scam address list expanded** (7 → 652 addresses): Sourced from [MyEtherWallet/ethereum-lists](https://github.com/MyEtherWallet/ethereum-lists) darklist (MIT license). Checked synchronously before sending. 2. **Contract address warning**: When the recipient is a smart contract (detected via `getCode`), shows a warning that sending directly to a contract may result in permanent loss of funds. 3. **Null/burn address warning**: Detects known burn addresses (`0x0000...0000`, `0x...dead`, `0x...deadbeef`) and warns that funds are permanently destroyed. 4. **No-history warning** (existing from #98): Unchanged, still shows for EOAs with zero transaction history. All warnings use reserved-space `visibility:hidden/visible` elements — no layout shift, no animations. closes #114 Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de> Co-authored-by: user <user@Mac.lan guest wan> Co-authored-by: clawbot <clawbot@eeqj.de> Reviewed-on: #118 Co-authored-by: clawbot <sneak+clawbot@sneak.cloud> Co-committed-by: clawbot <sneak+clawbot@sneak.cloud>
216 lines
6.0 KiB
JavaScript
216 lines
6.0 KiB
JavaScript
// Domain-based phishing detection using a vendored blocklist with delta updates.
|
|
//
|
|
// A community-maintained phishing domain blocklist is vendored in
|
|
// phishingBlocklist.json and bundled at build time. At runtime, we fetch
|
|
// the live list periodically and keep only the delta (new entries not in
|
|
// the vendored list) in memory. This keeps runtime memory usage small.
|
|
//
|
|
// The domain-checker checks the in-memory delta first (fresh/recent scam
|
|
// sites), then falls back to the vendored list.
|
|
//
|
|
// If the delta is under 256 KiB it is persisted to localStorage so it
|
|
// survives extension/service-worker restarts.
|
|
|
|
const vendoredConfig = require("./phishingBlocklist.json");
|
|
|
|
const BLOCKLIST_URL =
|
|
"https://raw.githubusercontent.com/MetaMask/eth-phishing-detect/main/src/config.json";
|
|
|
|
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
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 set — built once from the bundled JSON.
|
|
const vendoredBlacklist = new Set(
|
|
(vendoredConfig.blacklist || []).map((d) => d.toLowerCase()),
|
|
);
|
|
|
|
// Delta set — only entries from live list that are NOT in vendored.
|
|
let deltaBlacklist = new Set();
|
|
let lastFetchTime = 0;
|
|
let fetchPromise = null;
|
|
let refreshTimer = null;
|
|
|
|
/**
|
|
* Load delta entries from localStorage on startup.
|
|
* Called once during module initialization in the background script.
|
|
*/
|
|
function loadDeltaFromStorage() {
|
|
try {
|
|
const raw = localStorage.getItem(DELTA_STORAGE_KEY);
|
|
if (!raw) return;
|
|
const data = JSON.parse(raw);
|
|
if (data.blacklist && Array.isArray(data.blacklist)) {
|
|
deltaBlacklist = new Set(
|
|
data.blacklist.map((d) => d.toLowerCase()),
|
|
);
|
|
}
|
|
} catch {
|
|
// localStorage unavailable or corrupt — start empty
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Persist delta to localStorage if it fits within MAX_DELTA_BYTES.
|
|
*/
|
|
function saveDeltaToStorage() {
|
|
try {
|
|
const data = {
|
|
blacklist: Array.from(deltaBlacklist),
|
|
};
|
|
const json = JSON.stringify(data);
|
|
if (json.length < MAX_DELTA_BYTES) {
|
|
localStorage.setItem(DELTA_STORAGE_KEY, json);
|
|
} else {
|
|
// Too large — remove stale key if present
|
|
localStorage.removeItem(DELTA_STORAGE_KEY);
|
|
}
|
|
} catch {
|
|
// localStorage unavailable — skip silently
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load a pre-parsed config and compute the delta against the vendored list.
|
|
* Used for both live fetches and testing.
|
|
*
|
|
* @param {{ blacklist?: string[] }} config
|
|
*/
|
|
function loadConfig(config) {
|
|
const liveBlacklist = (config.blacklist || []).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)),
|
|
);
|
|
|
|
lastFetchTime = Date.now();
|
|
saveDeltaToStorage();
|
|
}
|
|
|
|
/**
|
|
* Generate hostname variants for subdomain matching.
|
|
* "sub.evil.com" yields ["sub.evil.com", "evil.com"].
|
|
*
|
|
* @param {string} hostname
|
|
* @returns {string[]}
|
|
*/
|
|
function hostnameVariants(hostname) {
|
|
const h = hostname.toLowerCase();
|
|
const variants = [h];
|
|
const parts = h.split(".");
|
|
// Parent domains: a.b.c.d -> b.c.d, c.d
|
|
for (let i = 1; i < parts.length - 1; i++) {
|
|
variants.push(parts.slice(i).join("."));
|
|
}
|
|
return variants;
|
|
}
|
|
|
|
/**
|
|
* Check if a hostname is on the phishing blocklist.
|
|
* Checks delta first (fresh/recent scam sites), then vendored list.
|
|
*
|
|
* @param {string} hostname - The hostname to check.
|
|
* @returns {boolean}
|
|
*/
|
|
function isPhishingDomain(hostname) {
|
|
if (!hostname) return false;
|
|
const variants = hostnameVariants(hostname);
|
|
|
|
// Check delta blacklist first (fresh/recent scam sites), then vendored
|
|
for (const v of variants) {
|
|
if (deltaBlacklist.has(v) || vendoredBlacklist.has(v)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Fetch the latest blocklist and compute delta against vendored data.
|
|
* De-duplicates concurrent fetches. Results are cached for CACHE_TTL_MS.
|
|
*
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async function updatePhishingList() {
|
|
// Skip if recently fetched
|
|
if (Date.now() - lastFetchTime < CACHE_TTL_MS && lastFetchTime > 0) {
|
|
return;
|
|
}
|
|
|
|
// De-duplicate concurrent calls
|
|
if (fetchPromise) return fetchPromise;
|
|
|
|
fetchPromise = (async () => {
|
|
try {
|
|
const resp = await fetch(BLOCKLIST_URL);
|
|
if (!resp.ok) throw new Error("HTTP " + resp.status);
|
|
const config = await resp.json();
|
|
loadConfig(config);
|
|
} catch {
|
|
// Silently fail — vendored list still provides coverage.
|
|
// We'll retry next time.
|
|
} finally {
|
|
fetchPromise = null;
|
|
}
|
|
})();
|
|
|
|
return fetchPromise;
|
|
}
|
|
|
|
/**
|
|
* Start periodic refresh of the phishing list.
|
|
* Should be called once from the background script on startup.
|
|
*/
|
|
function startPeriodicRefresh() {
|
|
if (refreshTimer) return;
|
|
refreshTimer = setInterval(updatePhishingList, REFRESH_INTERVAL_MS);
|
|
}
|
|
|
|
/**
|
|
* Return the total blocklist size (vendored + delta) for diagnostics.
|
|
*
|
|
* @returns {number}
|
|
*/
|
|
function getBlocklistSize() {
|
|
return vendoredBlacklist.size + deltaBlacklist.size;
|
|
}
|
|
|
|
/**
|
|
* Return the delta blocklist size for diagnostics.
|
|
*
|
|
* @returns {number}
|
|
*/
|
|
function getDeltaSize() {
|
|
return deltaBlacklist.size;
|
|
}
|
|
|
|
/**
|
|
* Reset internal state (for testing).
|
|
*/
|
|
function _reset() {
|
|
deltaBlacklist = new Set();
|
|
lastFetchTime = 0;
|
|
fetchPromise = null;
|
|
if (refreshTimer) {
|
|
clearInterval(refreshTimer);
|
|
refreshTimer = null;
|
|
}
|
|
}
|
|
|
|
// Load persisted delta on module initialization
|
|
loadDeltaFromStorage();
|
|
|
|
module.exports = {
|
|
isPhishingDomain,
|
|
updatePhishingList,
|
|
startPeriodicRefresh,
|
|
loadConfig,
|
|
getBlocklistSize,
|
|
getDeltaSize,
|
|
hostnameVariants,
|
|
_reset,
|
|
// Exposed for testing only
|
|
_getVendoredBlacklistSize: () => vendoredBlacklist.size,
|
|
_getDeltaBlacklist: () => deltaBlacklist,
|
|
};
|