Files
AutistMask/src/popup/views/settingsAddToken.js
clawbot c8c2af0c6b
All checks were successful
check / check (push) Successful in 28s
e2e / e2e-chrome (push) Successful in 1m10s
e2e / e2e-firefox (push) Successful in 22s
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.
2026-08-20 13:47:28 +02:00

163 lines
5.7 KiB
JavaScript

const { $, showView, showFlash, escapeHtml, goBack } = require("./helpers");
const { getTopTokens } = require("../../shared/tokenList");
const { state, saveState } = require("../../shared/state");
const { lookupTokenInfo } = require("../../shared/balances");
const { isScamAddress } = require("../../shared/scamlist");
const { log } = require("../../shared/log");
let ctx;
function isTracked(address) {
const lower = address.toLowerCase();
return state.trackedTokens.some((t) => t.address.toLowerCase() === lower);
}
function tokenLabel(t) {
return t.name ? t.name + " (" + t.symbol + ")" : t.symbol;
}
function renderTop10() {
const el = $("settings-addtoken-top10");
el.innerHTML = getTopTokens(10)
.map((t) => {
const tracked = isTracked(t.address);
const cls = tracked
? "border border-border px-1 text-xs opacity-40 cursor-default"
: "border border-border px-1 hover:bg-fg hover:text-bg cursor-pointer text-xs";
return (
`<button class="settings-addtoken-quick ${cls}"` +
` data-address="${escapeHtml(t.address)}"` +
` data-symbol="${escapeHtml(t.symbol)}"` +
` data-decimals="${escapeHtml(t.decimals)}"` +
` data-name="${escapeHtml(t.name || "")}"` +
`${tracked ? " disabled" : ""}>${escapeHtml(t.symbol)}</button>`
);
})
.join("");
el.querySelectorAll(".settings-addtoken-quick:not([disabled])").forEach(
(btn) => {
btn.addEventListener("click", async () => {
const token = {
address: btn.dataset.address,
symbol: btn.dataset.symbol,
decimals: parseInt(btn.dataset.decimals, 10),
name: btn.dataset.name || btn.dataset.symbol,
};
state.trackedTokens.push(token);
await saveState();
showFlash("Added " + token.symbol);
renderTop10();
renderDropdown();
ctx.doRefreshAndRender();
});
},
);
}
function renderDropdown() {
const sel = $("settings-addtoken-select");
const tokens = getTopTokens(100);
let html = '<option value="">-- select --</option>';
for (const t of tokens) {
const tracked = isTracked(t.address);
const label = tokenLabel(t) + (tracked ? " (tracked)" : "");
html +=
`<option value="${escapeHtml(t.address)}"` +
` data-symbol="${escapeHtml(t.symbol)}"` +
` data-decimals="${escapeHtml(t.decimals)}"` +
` data-name="${escapeHtml(t.name || "")}"` +
`${tracked ? " disabled" : ""}>${escapeHtml(label)}</option>`;
}
sel.innerHTML = html;
}
function show() {
$("settings-addtoken-address").value = "";
$("settings-addtoken-info").textContent = "";
$("settings-addtoken-info").style.visibility = "hidden";
renderTop10();
renderDropdown();
showView("settings-addtoken");
}
function init(_ctx) {
ctx = _ctx;
$("btn-settings-addtoken-back").addEventListener("click", () => {
goBack();
});
$("btn-settings-addtoken-select").addEventListener("click", async () => {
const sel = $("settings-addtoken-select");
const opt = sel.options[sel.selectedIndex];
if (!opt || !opt.value) {
showFlash("Please select a token.");
return;
}
if (isTracked(opt.value)) {
showFlash("Already tracked.");
return;
}
const token = {
address: opt.value,
symbol: opt.dataset.symbol,
decimals: parseInt(opt.dataset.decimals, 10),
name: opt.dataset.name || opt.dataset.symbol,
};
state.trackedTokens.push(token);
await saveState();
showFlash("Added " + token.symbol);
renderTop10();
renderDropdown();
ctx.doRefreshAndRender();
});
$("btn-settings-addtoken-manual").addEventListener("click", async () => {
const addr = $("settings-addtoken-address").value.trim();
if (!addr || !addr.startsWith("0x")) {
showFlash(
"Please enter a valid contract address starting with 0x.",
);
return;
}
if (isTracked(addr)) {
showFlash("Already tracked.");
return;
}
if (isScamAddress(addr)) {
showFlash("This address is on a known scam/fraud list.");
return;
}
const infoEl = $("settings-addtoken-info");
infoEl.textContent = "Looking up token...";
infoEl.style.visibility = "visible";
log.debugf("Looking up token contract", addr);
try {
const info = await lookupTokenInfo(addr, state.rpcUrl);
log.infof("Adding token", info.symbol, addr);
state.trackedTokens.push({
address: addr,
symbol: info.symbol,
decimals: info.decimals,
name: info.name,
});
await saveState();
showFlash("Added " + info.symbol);
$("settings-addtoken-address").value = "";
infoEl.textContent = "";
infoEl.style.visibility = "hidden";
renderTop10();
renderDropdown();
ctx.doRefreshAndRender();
} catch (e) {
const detail = e.shortMessage || e.message || String(e);
log.errorf("Token lookup failed for", addr, detail);
showFlash(detail);
infoEl.textContent = "";
infoEl.style.visibility = "hidden";
}
});
}
module.exports = { init, show };