harden: escape every interpolation into popup innerHTML, and add default-src to both manifests (closes #307)
All checks were successful
check / check (push) Successful in 28s
e2e / e2e-chrome (push) Successful in 1m10s
e2e / e2e-firefox (push) Successful in 22s

A hostile ERC-20's symbol() reached an innerHTML string unescaped, and neither
manifest declared default-src, so an attacker deploying a token with 1,000+
holders and airdropping one unit could render a full-viewport cross-origin
iframe over the wallet's own UI, on screens where the user types their
password.

escapeHtml is now a pure string replace over & < > " ' — the old version
round-tripped through textContent, which escapes neither quote, while already
being used inside data-copy="...". All 19 files in src/popup/views/ were
audited: beyond the reported symbol site, the explorer-supplied directionLabel
in all three transaction lists, wallet.name, addr.ensName, the blockie data:
URIs and two ad-hoc quote-only escapes were also unescaped. Explorer URLs now
go through one helper that percent-encodes the path segment.

Both manifests add default-src 'self', frame-src 'none', form-action 'none' and
base-uri 'none'. Three loosenings are pinned in tests/manifest.test.js and
justified in README.md: style-src 'unsafe-inline' (39 static style attributes;
Firefox implements neither style-src-attr nor 'unsafe-hashes'), img-src data:
(blockies), connect-src https: http: (user-configurable RPC).

Note frame-src 'none' blocks a frame loading, not the element existing, so the
zero-iframe assertion is a claim about the escaping alone; the test asserts the
element count and the literal rendered text separately, taking the count before
any click an overlay could intercept.

Verified: make check 39 suites / 811 tests, test-e2e 55/55 including the
WebAssembly-under-CSP assertion, test-e2e-firefox 8/8, zero CSP violations
asserted rather than merely unobserved. Reverting only balanceLine's
interpolation reproduces the attack as 2 iframes on the address screen.
This commit was merged in pull request #327.
This commit is contained in:
2026-08-20 13:47:28 +02:00
parent 59f68b8859
commit c8c2af0c6b
24 changed files with 706 additions and 97 deletions

View File

@@ -9,10 +9,12 @@ const {
attachCopyHandlers,
copyableHtml,
etherscanLinkHtml,
explorerUrl,
displaySymbol,
clearViewStack,
} = require("./helpers");
const { TOKEN_BY_ADDRESS } = require("../../shared/tokenList");
const { state, currentNetwork } = require("../../shared/state");
const { state } = require("../../shared/state");
const { getProvider } = require("../../shared/balances");
const { log } = require("../../shared/log");
@@ -62,13 +64,13 @@ function toAddressHtml(address) {
}
function txHashHtml(hash) {
const link = `${currentNetwork().explorerUrl}/tx/${hash}`;
const link = explorerUrl("tx", hash);
return copyableHtml(hash, "break-all") + etherscanLinkHtml(link);
}
function blockNumberHtml(blockNumber) {
const num = String(blockNumber);
const link = `${currentNetwork().explorerUrl}/block/${num}`;
const link = explorerUrl("block", num);
return copyableHtml(num) + etherscanLinkHtml(link);
}
@@ -80,7 +82,10 @@ function startWait(txInfo, txHash, broadcastTime, pollNow) {
endWait();
const id = waitId;
const symbol = txInfo.token === "ETH" ? "ETH" : txInfo.tokenSymbol || "?";
const symbol =
txInfo.token === "ETH"
? "ETH"
: displaySymbol(txInfo.tokenSymbol || "?");
$("wait-tx-summary").textContent = txInfo.amount + " " + symbol;
$("wait-tx-to").innerHTML = toAddressHtml(txInfo.to);
$("wait-tx-hash").innerHTML = txHashHtml(txHash);
@@ -211,7 +216,10 @@ function restoreWait() {
function showSuccess(txInfo, txHash, blockNumber) {
endWait();
const symbol = txInfo.token === "ETH" ? "ETH" : txInfo.tokenSymbol || "?";
const symbol =
txInfo.token === "ETH"
? "ETH"
: displaySymbol(txInfo.tokenSymbol || "?");
state.viewData = {
amount: txInfo.amount,
symbol: symbol,
@@ -299,7 +307,10 @@ function renderSuccess() {
function showError(txInfo, txHash, message) {
endWait();
const symbol = txInfo.token === "ETH" ? "ETH" : txInfo.tokenSymbol || "?";
const symbol =
txInfo.token === "ETH"
? "ETH"
: displaySymbol(txInfo.tokenSymbol || "?");
state.viewData = {
amount: txInfo.amount,
symbol: symbol,