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 three literals shipped code cannot avoid — two provider-shim identifiers in src/content/inpage.js and one ERC-20's on-chain name in src/shared/tokenList.js. Each is permitted only at the path that carries it, and at the emitted paths that path is bundled into, so a literal appearing anywhere else fails like any other occurrence. 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.
302 lines
11 KiB
Bash
Executable File
302 lines
11 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 two provider-shim identifiers in src/content/inpage.js. Protocol
|
|
# identifiers dApps feature-detect on; renaming them does not rename them in
|
|
# their code, it only stops this wallet working on their sites.
|
|
# - the on-chain name of the MUSD ERC-20 in src/shared/tokenList.js. It is not
|
|
# what backs symbol-spoof detection — that reads symbol and address — but
|
|
# the wallet already surfaces the on-chain name of any token the user holds
|
|
# (src/shared/balances.js), and this contract's on-chain name is that
|
|
# string, so censoring the repo cannot stop the wallet displaying it.
|
|
# Dropping the entry instead would cost the user MUSD spoof detection.
|
|
#
|
|
# Everything else fails, in the working tree and under dist/. The last two are
|
|
# literals rather than whole files, so they are enforced by counting, and each
|
|
# literal is scoped to the path allowed to carry it: a file may contain the name
|
|
# only as many times as it contains the literals permitted *there*, and zero
|
|
# times anywhere else. The emitted bundles carry them too, so a plain "the name
|
|
# must not appear in dist/" could never have passed.
|
|
#
|
|
# The name itself is not written in this file. 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 / make_literals_file.
|
|
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
|
|
}
|
|
|
|
make_literals_file() {
|
|
ALLOWED_LITERALS_FILE="$(mktemp \
|
|
"${TMPDIR:-/tmp}/autistmask-censored.XXXXXX")" ||
|
|
fail "could not create a temporary file, so nothing was scanned."
|
|
}
|
|
|
|
# The literals $1 may carry, and nothing else may. Each contains the name
|
|
# exactly once, which is what makes counting them sound; each is scoped to its
|
|
# path, so a file with no business carrying the name fails even when it spells
|
|
# it the way shipped code has to. Scoping is the point: permitting these
|
|
# literals in any file is what once let this check pass its own prose.
|
|
#
|
|
# The emitted paths are listed next to the sources they come from. If the
|
|
# bundler moves one, this goes red and the new path gets added deliberately,
|
|
# rather than a wildcard over dist/ covering whatever lands there.
|
|
allowed_literals_for() {
|
|
: >"$ALLOWED_LITERALS_FILE"
|
|
case "$1" in
|
|
src/content/inpage.js | dist/*/src/content/inpage.js)
|
|
printf 'is%s\n_%s\n' "$NAME" "$NAME" >"$ALLOWED_LITERALS_FILE"
|
|
;;
|
|
src/shared/tokenList.js | dist/*/src/background/index.js | \
|
|
dist/*/src/popup/index.js)
|
|
printf '%s USD\n' "$NAME" >"$ALLOWED_LITERALS_FILE"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# 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
|
|
|
|
# No literal is permitted at this path, so every occurrence is a violation.
|
|
# Handled here rather than by grep, which is not required to say anything
|
|
# useful about an empty pattern file.
|
|
if [ ! -s "$ALLOWED_LITERALS_FILE" ]; 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
|
|
# dist/ arrives absolute (find) and the worktree relative (git
|
|
# ls-files). The allowlist is keyed on repo-relative paths, so both
|
|
# forms are reduced to one before anything is decided about them.
|
|
_rel="$_file"
|
|
case "$_rel" in
|
|
"$ROOT"/*) _rel="${_rel#"$ROOT"/}" ;;
|
|
esac
|
|
|
|
case "$_rel" in
|
|
script/vendor-blocklist) continue ;;
|
|
esac
|
|
[ -f "$_file" ] || continue
|
|
|
|
allowed_literals_for "$_rel"
|
|
count_matches "$_file"
|
|
[ "$TOTAL" -gt "$ALLOWED" ] || continue
|
|
|
|
FAILED=$((FAILED + 1))
|
|
echo "check-censored: $_rel: $TOTAL occurrence(s) of the name," \
|
|
"$ALLOWED of them allowed at this path" >&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
|
|
make_literals_file
|
|
scan_paths "$@"
|
|
return $?
|
|
fi
|
|
|
|
require_dist=no
|
|
case "${1-}" in
|
|
"") ;;
|
|
--require-dist) require_dist=yes ;;
|
|
*) usage ;;
|
|
esac
|
|
|
|
extract_name
|
|
make_literals_file
|
|
|
|
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 "$@"
|