The blocklist URL in shipped code named a competitor and pointed at a moving ref, and the extension re-fetched from it every 24 hours, which also meant a third party decided what this wallet warns about. All of that is gone. script/vendor-blocklist fetches upstream at a pinned commit, verifies the sha256 of the bytes that commit serves, and writes src/shared/phishingBlocklist.json. It is build-time tooling, never shipped, and the one place in the repo that names the upstream project; a source reference nobody can verify is not a source reference. The artifact stores truncated sha256 digests rather than domain names. That is what censors it: the previous file contained the competitor's name 6,475 times, as phishing domains impersonating them, and not one of those domains is dropped. It also makes lookups a binary search over a fixed-width string, so nothing is built at module load — which matters on MV3, where the worker re-evaluates the module on every wake — and takes the file from 8.7 MB to 1.7 MB. script/check-censored enforces the rest: it reads the name out of the vendoring script rather than repeating it, and fails on any occurrence in the working tree or under dist/ that is not one of the two literals shipped code cannot avoid. It runs in make check, which inspects dist/ when there is one and says loudly when there is not, and again with --require-dist at the end of every make build. Removing the runtime fetch retires the delta, the extension-storage persistence and the 24-hour alarm from #158. A retired alarm is now cleared rather than left waking the worker forever on installs that already have it. The e2e suite drives the warning end to end from a real blocklisted origin served as a real http(s) site, with a control asserting the banner stays hidden for one that is not listed. Its service-worker interception canary needed a new anchor, since the startup fetch it used to watch for no longer happens: it now wakes the worker with a message and asks it for one throwaway fetch. LICENSE no longer cites a repository that returns 404. eslint.config.js gains one block: script/lib/ holds node programs the shell entrypoints call, and without it they lint with no globals at all.
268 lines
9.0 KiB
Bash
Executable File
268 lines
9.0 KiB
Bash
Executable File
#!/bin/sh
|
|
# script/check-censored: assert that the competitor name RULES.md bars appears
|
|
# nowhere in this repo, and nowhere in the built extension, except where it is
|
|
# deliberate. Our own extension to scripts-to-rule-them-all, run from
|
|
# script/check and from make build.
|
|
#
|
|
# Where the name is allowed, and why each one is not negotiable away:
|
|
#
|
|
# - script/vendor-blocklist. Build-time tooling, never shipped. A pinned
|
|
# source reference that does not say what the source is cannot be verified
|
|
# by anyone, so it names it. Whole-file exemption.
|
|
# - the literal `isMetaMask` and the `_metamask` object in
|
|
# src/content/inpage.js. A protocol identifier dApps feature-detect on;
|
|
# renaming it does not rename it in their code, it only stops this wallet
|
|
# working on their sites.
|
|
# - the literal `MetaMask USD` in src/shared/tokenList.js. The on-chain name
|
|
# of an ERC-20 the user may hold, recorded next to its address and symbol,
|
|
# which is what lets symbol-spoofing detection tell the real one from a
|
|
# forgery. Altering it would misidentify an asset to its owner.
|
|
#
|
|
# Everything else fails, in the working tree and under dist/. The last two are
|
|
# literals rather than files, so they are enforced by counting: a file may
|
|
# contain the name only as many times as it contains those exact strings. The
|
|
# emitted bundles carry both, so a plain "the name must not appear in dist/"
|
|
# could never have passed.
|
|
#
|
|
# The name itself is not written here. script/vendor-blocklist is the one place
|
|
# in this repo that defines it, and this reads it back out of there — so the
|
|
# repo-wide grep this check exists to enforce keeps returning exactly the files
|
|
# named above, and this file is not one of them.
|
|
set -eu
|
|
|
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
|
|
|
# Absolute path to this script, resolved before anything cd's anywhere: the
|
|
# scan half runs in a re-invocation through xargs, so that the paths it works on
|
|
# arrive as arguments and cannot be reshaped by field splitting on the way in.
|
|
SELF="$(cd "$(dirname "$0")" && pwd -P)/$(basename "$0")"
|
|
|
|
# Internal re-entry flag. Not part of the command-line interface.
|
|
SCAN_FLAG="--scan-paths"
|
|
|
|
VENDOR_SCRIPT="$ROOT/script/vendor-blocklist"
|
|
|
|
# Set by extract_name / write_allowed_literals.
|
|
NAME=""
|
|
ALLOWED_LITERALS_FILE=""
|
|
|
|
FAILED=0
|
|
|
|
cleanup() {
|
|
[ -z "$ALLOWED_LITERALS_FILE" ] || rm -f "$ALLOWED_LITERALS_FILE"
|
|
}
|
|
trap cleanup EXIT INT TERM
|
|
|
|
fail() {
|
|
echo "check-censored: FAIL: $*" >&2
|
|
exit 1
|
|
}
|
|
|
|
# The name, taken from the single place that defines it. A check scanning for a
|
|
# pattern it failed to read would pass against anything, so this refuses to
|
|
# continue unless it got something that looks like the definition.
|
|
extract_name() {
|
|
[ -f "$VENDOR_SCRIPT" ] ||
|
|
fail "$VENDOR_SCRIPT is missing, and it is where the name being
|
|
checked for is defined. Nothing was scanned."
|
|
|
|
NAME="$(grep -m1 '^UPSTREAM_ORG=' "$VENDOR_SCRIPT" | cut -d'"' -f2)" ||
|
|
fail "could not read UPSTREAM_ORG from $VENDOR_SCRIPT. Nothing was
|
|
scanned."
|
|
|
|
case "$NAME" in
|
|
"" | *[!A-Za-z0-9]*)
|
|
fail "UPSTREAM_ORG in $VENDOR_SCRIPT did not yield a plain name
|
|
(got: '$NAME'). Scanning for that would prove nothing. Nothing was
|
|
scanned."
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# Occurrences allowed anywhere, including in the emitted bundles. Each contains
|
|
# the name exactly once, which is what makes counting them sound.
|
|
write_allowed_literals() {
|
|
ALLOWED_LITERALS_FILE="$(mktemp \
|
|
"${TMPDIR:-/tmp}/autistmask-censored.XXXXXX")" ||
|
|
fail "could not create a temporary file, so nothing was scanned."
|
|
{
|
|
echo "is$NAME"
|
|
echo "_$NAME"
|
|
echo "$NAME USD"
|
|
} >"$ALLOWED_LITERALS_FILE"
|
|
}
|
|
|
|
# How many times does $1 contain the name (TOTAL), and how many of those are one
|
|
# of the allowed literals (ALLOWED)? Same discipline the rest of this repo's
|
|
# shell checks apply to grep: exit 0 and 1 are answers about the file, anything
|
|
# else means the file was not searched and is not an answer at all.
|
|
count_matches() {
|
|
_cm_status=0
|
|
_cm_out="$(grep -a -o -i -F -e "$NAME" -- "$1")" || _cm_status=$?
|
|
case "$_cm_status" in
|
|
0) TOTAL="$(printf '%s\n' "$_cm_out" | grep -c .)" ;;
|
|
1) TOTAL=0 ;;
|
|
*)
|
|
fail "grep exited $_cm_status reading $1, so the file was never
|
|
searched and nothing was established about it. That is a permissions or I/O
|
|
fault, not a clean file. Refusing to report success."
|
|
;;
|
|
esac
|
|
|
|
if [ "$TOTAL" -eq 0 ]; then
|
|
ALLOWED=0
|
|
return 0
|
|
fi
|
|
|
|
_cm_status=0
|
|
_cm_out="$(grep -a -o -i -F -f "$ALLOWED_LITERALS_FILE" -- "$1")" ||
|
|
_cm_status=$?
|
|
case "$_cm_status" in
|
|
0) ALLOWED="$(printf '%s\n' "$_cm_out" | grep -c .)" ;;
|
|
1) ALLOWED=0 ;;
|
|
*)
|
|
fail "grep exited $_cm_status matching the allowed literals in $1.
|
|
Refusing to report success."
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# The per-path half, run in a re-invocation of this script so it uses the same
|
|
# counting as everything else rather than a second copy of it.
|
|
scan_paths() {
|
|
for _file in "$@"; do
|
|
case "$_file" in
|
|
"$VENDOR_SCRIPT" | "script/vendor-blocklist") continue ;;
|
|
esac
|
|
[ -f "$_file" ] || continue
|
|
|
|
count_matches "$_file"
|
|
[ "$TOTAL" -gt "$ALLOWED" ] || continue
|
|
|
|
FAILED=$((FAILED + 1))
|
|
echo "check-censored: $_file: $TOTAL occurrence(s) of the name," \
|
|
"$ALLOWED of them allowed" >&2
|
|
grep -a -n -i -F -e "$NAME" -- "$_file" | cut -c1-140 | head -5 >&2
|
|
done
|
|
[ "$FAILED" -eq 0 ]
|
|
}
|
|
|
|
# Hand a NUL-delimited listing to the scan half. Returns non-zero if any path
|
|
# failed, or if the scan could not be run at all.
|
|
scan_listing() {
|
|
xargs -0 "$SELF" "$SCAN_FLAG" <"$1"
|
|
}
|
|
|
|
# Every file git tracks, plus everything untracked and not ignored: the working
|
|
# tree as a reviewer would see it, and never node_modules or dist/ (both are
|
|
# ignored; dist/ is walked separately below).
|
|
check_worktree() {
|
|
_list="$(mktemp "${TMPDIR:-/tmp}/autistmask-censored-tree.XXXXXX")" ||
|
|
fail "could not create a temporary file, so nothing was scanned."
|
|
_status=0
|
|
git ls-files -z --cached --others --exclude-standard >"$_list" ||
|
|
_status=$?
|
|
[ "$_status" -eq 0 ] || {
|
|
rm -f "$_list"
|
|
fail "git ls-files exited $_status, so the working tree was never
|
|
enumerated and nothing was established about it."
|
|
}
|
|
|
|
# Repo-relative paths. The scan half cd's to the repo root before it opens
|
|
# anything, so they reach it intact and unjoined.
|
|
WORKTREE_COUNT="$(tr -dc '\0' <"$_list" | wc -c | tr -d ' ')"
|
|
|
|
_status=0
|
|
scan_listing "$_list" || _status=$?
|
|
rm -f "$_list"
|
|
return "$_status"
|
|
}
|
|
|
|
check_dist() {
|
|
_list="$(mktemp "${TMPDIR:-/tmp}/autistmask-censored-dist.XXXXXX")" ||
|
|
fail "could not create a temporary file, so dist/ was not scanned."
|
|
_status=0
|
|
find "$ROOT/dist" -type f -print0 >"$_list" || _status=$?
|
|
[ "$_status" -eq 0 ] || {
|
|
rm -f "$_list"
|
|
fail "find exited $_status enumerating dist/, so part of the emitted
|
|
tree was never walked and an unchecked file there went unchecked. Refusing
|
|
to report success."
|
|
}
|
|
|
|
DIST_COUNT="$(tr -dc '\0' <"$_list" | wc -c | tr -d ' ')"
|
|
|
|
_status=0
|
|
scan_listing "$_list" || _status=$?
|
|
rm -f "$_list"
|
|
return "$_status"
|
|
}
|
|
|
|
usage() {
|
|
echo "usage: script/check-censored [--require-dist]" >&2
|
|
exit 2
|
|
}
|
|
|
|
main() {
|
|
cd "$ROOT"
|
|
|
|
# Internal re-entry from scan_listing's xargs.
|
|
if [ "${1-}" = "$SCAN_FLAG" ]; then
|
|
shift
|
|
extract_name
|
|
write_allowed_literals
|
|
scan_paths "$@"
|
|
return $?
|
|
fi
|
|
|
|
require_dist=no
|
|
case "${1-}" in
|
|
"") ;;
|
|
--require-dist) require_dist=yes ;;
|
|
*) usage ;;
|
|
esac
|
|
|
|
extract_name
|
|
write_allowed_literals
|
|
|
|
echo "Checking for censored names..."
|
|
|
|
tree_status=0
|
|
check_worktree || tree_status=$?
|
|
|
|
dist_status=0
|
|
dist_inspected=no
|
|
DIST_COUNT=0
|
|
if [ -d "$ROOT/dist" ]; then
|
|
dist_inspected=yes
|
|
check_dist || dist_status=$?
|
|
fi
|
|
|
|
if [ "$tree_status" -ne 0 ] || [ "$dist_status" -ne 0 ]; then
|
|
fail "the name appears outside the deliberate exceptions (reported
|
|
above). See the header of script/check-censored for what is allowed and
|
|
why."
|
|
fi
|
|
|
|
if [ "$dist_inspected" = no ]; then
|
|
if [ "$require_dist" = yes ]; then
|
|
fail "there is no dist/ to inspect and this run was asked to
|
|
require one. Run make build."
|
|
fi
|
|
cat <<EOF
|
|
################################################################################
|
|
## WARNING: dist/ WAS NOT INSPECTED BY THIS RUN AND IS NOT PROVEN CLEAN BY IT.
|
|
## There is no dist/ in this tree. The working tree is clean, but a build can
|
|
## carry text no source file does — a dependency's, or a bundler's. Every
|
|
## make build runs this check again with dist/ required, so a release artifact
|
|
## is always covered; this run simply had none to look at.
|
|
################################################################################
|
|
EOF
|
|
fi
|
|
|
|
echo "check-censored: $WORKTREE_COUNT tracked file(s) inspected," \
|
|
"$DIST_COUNT file(s) under dist/"
|
|
}
|
|
|
|
main "$@"
|