// HTML escaping for values interpolated into an innerHTML string. // // Every view in src/popup/views/ builds markup by string concatenation, so // this is the only thing standing between a value the wallet did not author // and the extension's own DOM. The values that reach it are attacker // controlled by design: an ERC-20's symbol() and name() are whatever the // contract chooses to return, an ENS name is whatever the resolver returns, // and both arrive through the block explorer with no schema. // // It escapes both quote characters as well as the tag delimiters, because // the popup interpolates into attribute values as well as into element // text — copyableHtml() writes data-copy="..." and etherscanLinkHtml() // writes href="...". A `<`/`>`-only escape leaves an unquoted-attribute // break-out intact, and the round trip through a detached element's // textContent that used to implement this was exactly that escape: the // HTML serializer only escapes `&`, `<`, `>` and U+00A0 in a text node, // since a text node has no idea it is about to be pasted inside quotes. // // Deliberately a pure string function with no DOM dependency: it is called // on every rendered row, it is unit-testable without a document, and it // cannot be affected by the state of a document that an attacker-supplied // string has already been written into. const HTML_ESCAPES = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'", }; // `&` is escaped first by virtue of being in the same pass: a sequential // replace would re-escape the ampersands it had just introduced. function escapeHtml(s) { if (s === null || s === undefined) return ""; return String(s).replace(/[&<>"']/g, (c) => HTML_ESCAPES[c]); } module.exports = { escapeHtml, };