harden: escape every interpolation into popup innerHTML, and add default-src to both manifests (closes #307)
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:
@@ -10,6 +10,7 @@ const {
|
||||
showView,
|
||||
addressTitle,
|
||||
escapeHtml,
|
||||
displaySymbol,
|
||||
renderAddressHtml,
|
||||
attachCopyHandlers,
|
||||
goBack,
|
||||
@@ -57,7 +58,7 @@ function restore() {
|
||||
|
||||
function blockieHtml(address) {
|
||||
const src = makeBlockie(address);
|
||||
return `<img src="${src}" width="48" height="48" style="image-rendering:pixelated;border-radius:50%;display:inline-block">`;
|
||||
return `<img src="${escapeHtml(src)}" width="48" height="48" style="image-rendering:pixelated;border-radius:50%;display:inline-block">`;
|
||||
}
|
||||
|
||||
function confirmAddressHtml(address, ensName, title) {
|
||||
@@ -81,7 +82,11 @@ function show(txInfo) {
|
||||
feeWei = null;
|
||||
|
||||
const isErc20 = txInfo.token !== "ETH";
|
||||
const symbol = isErc20 ? txInfo.tokenSymbol || "?" : "ETH";
|
||||
// The raw symbol is the price-table key; the capped one is what the
|
||||
// screen says. Truncating before the lookup would silently drop the
|
||||
// price of any token whose symbol is long enough to be capped.
|
||||
const rawSymbol = isErc20 ? txInfo.tokenSymbol || "?" : "ETH";
|
||||
const symbol = displaySymbol(rawSymbol);
|
||||
|
||||
// Transaction type
|
||||
if (isErc20) {
|
||||
@@ -123,7 +128,7 @@ function show(txInfo) {
|
||||
|
||||
// Amount (with inline USD)
|
||||
const ethPrice = getPrice("ETH");
|
||||
const tokenPrice = getPrice(symbol);
|
||||
const tokenPrice = getPrice(rawSymbol);
|
||||
const amountNum = parseFloat(txInfo.amount);
|
||||
const price = isErc20 ? tokenPrice : ethPrice;
|
||||
const amountUsd = price ? amountNum * price : null;
|
||||
@@ -156,7 +161,12 @@ function show(txInfo) {
|
||||
warningsEl.innerHTML = localWarnings
|
||||
.map(
|
||||
(w) =>
|
||||
`<div class="border border-border border-dashed p-2 mb-1 text-xs font-bold">WARNING: ${w.message}</div>`,
|
||||
// Only the three hardcoded strings in
|
||||
// src/shared/addressWarnings.js reach this today, but
|
||||
// src/shared/etherscanLabels.js already builds a
|
||||
// `warning` out of scraped explorer markup, so this is
|
||||
// one wiring change away from carrying remote text.
|
||||
`<div class="border border-border border-dashed p-2 mb-1 text-xs font-bold">WARNING: ${escapeHtml(w.message)}</div>`,
|
||||
)
|
||||
.join("");
|
||||
warningsEl.style.visibility = "visible";
|
||||
@@ -206,7 +216,7 @@ function show(txInfo) {
|
||||
// touches already occupies its space, so re-running it never moves anything.
|
||||
function renderValidation(txInfo) {
|
||||
const isErc20 = txInfo.token !== "ETH";
|
||||
const symbol = isErc20 ? txInfo.tokenSymbol || "?" : "ETH";
|
||||
const symbol = isErc20 ? displaySymbol(txInfo.tokenSymbol || "?") : "ETH";
|
||||
|
||||
const { canSend, codes } = validateTransfer({
|
||||
isErc20,
|
||||
|
||||
Reference in New Issue
Block a user