feat: vendor and censor the phishing blocklist at build time (closes #219)
All checks were successful
check / check (push) Successful in 27s
e2e / e2e-chrome (push) Successful in 48s
e2e / e2e-firefox (push) Successful in 21s

This commit was merged in pull request #301.
This commit is contained in:
2026-08-17 10:05:56 +02:00
parent 8fcdd8a053
commit ff3387d8cf
25 changed files with 1354 additions and 232497 deletions

View File

@@ -9,13 +9,12 @@
//
// Service-worker coverage is not free: ctx.route() only sees worker
// traffic when PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS=1 is set in
// the environment, which script/test-e2e does. Without it the phishing
// blocklist fetch that src/background/index.js issues at worker startup
// silently reaches raw.githubusercontent.com on the open internet, and
// src/shared/phishingDomains.js swallows the failure so nothing surfaces
// it. That is not left to trust: waitForServiceWorkerTraffic() below
// backs the launch-time canary in harness.js, which fails the entire
// suite if worker requests stop being visible here.
// the environment, which script/test-e2e does. Without it every fetch the
// MV3 background worker makes — the JSON-RPC calls behind every approval
// in this suite among them — goes to the real internet unobserved. That is
// not left to trust: waitForServiceWorkerTraffic() below backs the
// launch-time canary in harness.js, which fails the entire suite if worker
// requests stop being visible here.
//
// Anything not explicitly stubbed here is aborted AND reported to the
// error collector, so a newly added outbound call shows up as a test
@@ -103,6 +102,22 @@ function word(value) {
const DAPP_ORIGIN = "https://dapp.e2e.test";
const DAPP_URL = DAPP_ORIGIN + "/";
// The same page, served from a hostname that is on the vendored phishing
// blocklist, so the phishing warning can be driven end to end against the real
// list rather than a stub of it. It is a live entry at the pinned upstream
// commit; upstream prunes, so a re-vendoring run that retires it turns the
// phishing test red, and the fix is a current entry, not a weaker assertion.
const PHISHING_DAPP_ORIGIN = "https://myetheywallet.com";
const PHISHING_DAPP_URL = PHISHING_DAPP_ORIGIN + "/";
// A request the harness asks the background service worker to make, purely so
// that worker interception can be proved before any test runs. Nothing in the
// extension fetches at startup any more — the blocklist is vendored at build
// time — so the canary in harness.js has no product traffic to anchor on and
// generates its own. See assertWorkerTrafficIntercepted().
const WORKER_PROBE_ORIGIN = "https://worker-probe.e2e.test";
const WORKER_PROBE_URL = WORKER_PROBE_ORIGIN + "/canary";
// Requests are parked rather than awaited. An approval prompt only exists
// while its call is in flight, so a test that awaited the promise could
// never drive the popup that has to settle it; start() files the promise
@@ -555,10 +570,9 @@ async function installNetworkStubs(ctx, opts) {
// E2E_TRACE_NETWORK=1 prints every request that reaches this handler,
// tagged [sw] when it originated in the background service worker.
// It exists so the isolation claim above can be re-checked by anyone
// in one command, without editing files: the phishing blocklist fetch
// showing up with an [sw] tag is the proof that the worker really is
// intercepted and that the raw.githubusercontent.com stub below is
// live code rather than decoration.
// in one command, without editing files: the canary probe and then
// every JSON-RPC call behind an approval showing up with an [sw] tag
// is the proof that the worker really is intercepted.
const trace = traceEnabled(process.env.E2E_TRACE_NETWORK);
// Regex rather than a glob so chrome-extension:// resource loads are
@@ -589,7 +603,11 @@ async function installNetworkStubs(ctx, opts) {
// trips run against a real http(s) origin — which is what makes the
// shipped content scripts inject at all — without any remote origin
// being involved.
if (url.origin === DAPP_ORIGIN && p === "/") {
if (
(url.origin === DAPP_ORIGIN ||
url.origin === PHISHING_DAPP_ORIGIN) &&
p === "/"
) {
return route.fulfill({
status: 200,
contentType: "text/html; charset=utf-8",
@@ -635,18 +653,10 @@ async function installNetworkStubs(ctx, opts) {
return jsonResponse(route, { Data: {} });
}
// MetaMask phishing blocklist
if (
url.hostname === "raw.githubusercontent.com" ||
p.endsWith("/eth-phishing-detect/main/src/config.json")
) {
return jsonResponse(route, {
version: 2,
tolerance: 2,
fuzzylist: [],
whitelist: [],
blacklist: [],
});
// The interception canary's own request. Answered with nothing: what
// is being observed is that it arrived here at all.
if (url.href === WORKER_PROBE_URL) {
return route.fulfill({ status: 204, body: "" });
}
// Best-effort Etherscan address labels: served as an empty page.
@@ -667,12 +677,12 @@ async function installNetworkStubs(ctx, opts) {
* Resolve with the first service-worker-originated request this
* handler saw, or null if none arrives within `ms`.
*
* The background worker fetches the phishing blocklist at
* startup, unconditionally, within about a second of the context
* coming up — so under working interception this resolves almost
* immediately. Nothing arriving means worker traffic is bypassing
* the handler entirely and going to the real internet, which the
* caller turns into a hard failure of the whole suite.
* The caller asks the worker for one request of its own (see
* WORKER_PROBE_URL) and then waits here, so under working
* interception this resolves almost immediately. Nothing arriving
* means worker traffic is bypassing the handler entirely and going
* to the real internet, which the caller turns into a hard failure
* of the whole suite.
*/
waitForServiceWorkerTraffic(ms) {
if (firstWorkerRequest) return Promise.resolve(firstWorkerRequest);
@@ -696,6 +706,9 @@ module.exports = {
DAPP_HTML,
DAPP_ORIGIN,
DAPP_URL,
PHISHING_DAPP_ORIGIN,
PHISHING_DAPP_URL,
WORKER_PROBE_URL,
FEE_ESTIMATE_WEI,
FEE_RESERVE_WEI,
STUB_COUNTERPARTY,