The token screen's decimals and holder count, the ETH price, every address total and each balance row's USD value went into innerHTML unescaped, against the rule at the top of src/popup/views/helpers.js. They are escaped now. None could carry markup, but formatUsd() writes a value under a cent as "< $0.01". displaySymbol() counts a symbol in code points, not UTF-16 units, so the cut never leaves half of an emoji, which rendered as U+FFFD. explorerLink() was already removed on next. Model: opus-5-5
77 lines
3.1 KiB
JavaScript
77 lines
3.1 KiB
JavaScript
// balanceLine() is the row that issue #307 was reported against: every
|
|
// screen that lists a holding renders through it, and the symbol it renders
|
|
// is whatever an ERC-20's symbol() returned. This asserts against the
|
|
// string it emits, which is what gets assigned to innerHTML.
|
|
//
|
|
// The browser half of the same claim — that a real Chrome renders that
|
|
// string as text and puts no iframe in the popup DOM — is in
|
|
// tests/e2e/run.js. This half runs inside the 20-second make test cap.
|
|
|
|
"use strict";
|
|
|
|
// helpers.js reaches for both at module scope through the modules it pulls
|
|
// in. Neither is exercised by anything asserted here.
|
|
global.chrome = {
|
|
storage: {
|
|
local: {
|
|
get: () => Promise.resolve({}),
|
|
set: () => Promise.resolve(),
|
|
},
|
|
},
|
|
runtime: { sendMessage: () => {} },
|
|
};
|
|
global.document = {
|
|
getElementById: () => null,
|
|
createElement: () => ({ style: {}, classList: { toggle() {} } }),
|
|
body: { prepend: () => {} },
|
|
addEventListener: () => {},
|
|
};
|
|
|
|
const { balanceLine } = require("../src/popup/views/helpers");
|
|
const { MAX_SYMBOL_LENGTH } = require("../src/shared/symbolDisplay");
|
|
|
|
// The payload from the issue's reproduction, verbatim.
|
|
const HOSTILE_SYMBOL =
|
|
'<iframe id="pwn" src="https://dapp.e2e.test/" ' +
|
|
'style="position:fixed;left:0;top:0;width:360px;height:600px;z-index:99999"></iframe>';
|
|
|
|
describe("balanceLine", () => {
|
|
test("emits a hostile symbol as text, not as an element", () => {
|
|
// Deliberately asserted on the escaping alone. The cap truncates
|
|
// this payload before its id attribute, so an assertion about the
|
|
// rest of the payload would pass on the cap and say nothing about
|
|
// the escape.
|
|
const html = balanceLine(HOSTILE_SYMBOL, 1, null, null);
|
|
expect(html).not.toContain("<iframe");
|
|
expect(html).toContain("<iframe");
|
|
});
|
|
|
|
test("caps the symbol before rendering it", () => {
|
|
const html = balanceLine("A".repeat(4096), 1, null, null);
|
|
expect(html).toContain("A".repeat(MAX_SYMBOL_LENGTH - 1) + "…");
|
|
expect(html).not.toContain("A".repeat(MAX_SYMBOL_LENGTH + 1));
|
|
});
|
|
|
|
// The token id lands inside data-token="...", so a quote in it is a
|
|
// way out of the attribute and into a new one.
|
|
test("keeps a quote-bearing token id inside its attribute", () => {
|
|
const html = balanceLine("TKN", 1, null, '" onclick="alert(1)');
|
|
expect(html).not.toContain('onclick="');
|
|
expect(html).toContain('data-token="" onclick="alert(1)"');
|
|
});
|
|
|
|
test("renders an ordinary holding unchanged", () => {
|
|
const html = balanceLine("USDC", 1.5, null, "0xabc");
|
|
expect(html).toContain("<span>USDC</span>");
|
|
expect(html).toContain("<span>1.5000</span>");
|
|
expect(html).toContain('data-token="0xabc"');
|
|
});
|
|
|
|
// formatUsd() writes a value under a cent as "< $0.01".
|
|
test("escapes the USD value along with the symbol", () => {
|
|
const html = balanceLine("USDC", 0.001, 1, null);
|
|
expect(html).toContain("< $0.01");
|
|
expect(html).not.toContain("< $0.01");
|
|
});
|
|
});
|