fix: render a hostile token symbol as text, and put a floor under the CSP (closes #307)
A token's symbol is whatever its symbol() returns, the block explorer passes it through unfiltered, and balanceLine() interpolated it into an innerHTML string. A token with the 1,000 holders the spam filter asks for, airdropped to the victim, could therefore paint a full-viewport cross-origin iframe over the wallet's own UI, on the screens where the user is used to typing their password. escapeHtml moves to the new src/shared/html.js as a pure string replace over &, <, >, " and '. The implementation it replaces round-tripped through a detached element's textContent, which escapes neither quote character, and it was already in use inside data-copy="..." and would have been inside href="...". Being pure also makes it testable without a DOM shim. Every interpolation into an innerHTML string across src/popup/views/ was audited rather than only the reported one. Also unescaped: the transaction lists' direction label (the explorer's method name, attacker-chosen for an attacker's contract), the wallet name and ENS name in the Home wallet list, the URL in the explorer link's href, the blockie data: URI, and the confirmation screen's warning line, which carries only fixed strings today but is one wiring change from carrying scraped explorer text. Explorer URLs are now built by one helper that percent-encodes the path segment, so a from/to out of explorer JSON cannot re-point the link. Where a value is a markup fragment this code just built, or a loop index, or a locally computed number, it stays bare; the rule and the reason are stated at the top of helpers.js. Both manifests now declare default-src 'self' with frame-src 'none'. Four directives had to stay looser than 'self' and none of them generalises: style-src needs 'unsafe-inline' because the popup sets presentation through style="..." attributes and Firefox has never implemented style-src-attr; img-src needs data: for the blockies; connect-src needs https: and http: because the RPC endpoint is user-configurable and a local node over http://127.0.0.1 is a supported configuration. frame-src, form-action and base-uri are named rather than inherited, because the last two do not fall back to default-src at all. tests/manifest.test.js now pins the whole directive set exactly, in both directions, and README.md carries the reasoning. Displayed symbols are capped at 12 characters, the bound lookupTokenInfo() already applied to a symbol read straight off a contract; the explorer path had none. The cap is a layout bound and is documented as not being the security control. isSpoofedSymbol() is untouched: it answers whether a symbol collides with a known ticker, which is a different question, and repurposing it here would have been the wrong control. Verified failing first, four ways. Restricting escapeHtml to & < > (the escape the old textContent round trip actually performed) fails 5 unit tests including the data-copy attribute break-out. Removing the length cap fails 3. Dropping default-src from manifest/chrome.json fails 2. Removing both the escape and the cap and running the full Chrome suite fails the new browser test with the attack reproduced: an <iframe id="pwn"> in the popup DOM, intercepting pointer events over the Back button.
This commit is contained in:
47
README.md
47
README.md
@@ -1464,10 +1464,44 @@ policy, but as of now there are none.
|
||||
|
||||
### Content Security Policy
|
||||
|
||||
Both manifests declare the same policy for extension pages —
|
||||
`script-src 'self' 'wasm-unsafe-eval'; object-src 'self'` — as an object under
|
||||
Both manifests declare the same policy for extension pages, as an object under
|
||||
`content_security_policy.extension_pages` in `manifest/chrome.json` (MV3) and as
|
||||
a bare string in `manifest/firefox.json` (MV2).
|
||||
a bare string in `manifest/firefox.json` (MV2):
|
||||
|
||||
```
|
||||
default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; object-src 'self';
|
||||
style-src 'self' 'unsafe-inline'; img-src 'self' data:;
|
||||
connect-src 'self' https: http:; frame-src 'none'; form-action 'none';
|
||||
base-uri 'none'
|
||||
```
|
||||
|
||||
`default-src 'self'` is the floor. Without it the policy governed script and
|
||||
plugins only, and everything else — frames above all — was unrestricted, which
|
||||
is what let an unescaped token symbol paint a cross-origin iframe over the
|
||||
wallet's own UI. Escaping is the primary fix for that (see
|
||||
`src/shared/html.js`); this is the second line, so an escape that does slip
|
||||
cannot reach the network.
|
||||
|
||||
Four directives are looser than `'self'`, each for a reason that does not
|
||||
generalise:
|
||||
|
||||
- `style-src 'unsafe-inline'` — `src/popup/index.html` and the view helpers set
|
||||
presentation through `style="..."` attributes, which CSP blocks without this.
|
||||
Chrome enforces `style-src` on attributes, not only on `<style>` blocks, and
|
||||
Firefox has never implemented `style-src-attr`, so there is no narrower
|
||||
spelling that works on both targets. It permits inline **style**; script stays
|
||||
under `script-src`, which does not allow `'unsafe-inline'`.
|
||||
- `img-src data:` — identicons are generated in the popup by
|
||||
`ethereum-blockies-base64` and assigned to `img.src` as `data:` PNGs.
|
||||
- `connect-src https: http:` — the RPC endpoint is user-configurable and a local
|
||||
node over `http://127.0.0.1` is a supported configuration, which the Firefox
|
||||
end-to-end suite depends on. The wallet's outbound traffic is constrained by
|
||||
what it is written to contact (see External Communication), not by this
|
||||
directive.
|
||||
- `frame-src 'none'`, `form-action 'none'`, `base-uri 'none'` — named rather
|
||||
than inherited. `form-action` and `base-uri` do not fall back to `default-src`
|
||||
at all, so they would have stayed unrestricted; `frame-src 'none'` is what
|
||||
refuses the framed-overlay attack outright.
|
||||
|
||||
`'wasm-unsafe-eval'` is there for one reason: libsodium. It ships a WebAssembly
|
||||
build and a `wasm2js` translation of it in one file, tries WASM first, and
|
||||
@@ -1485,9 +1519,10 @@ strings, not inline script, not remote script. Using it requires already
|
||||
executing script in an extension page, which is complete compromise on its own.
|
||||
`'unsafe-eval'` is a different proposition and is not granted.
|
||||
|
||||
The grant is pinned in both directions. `tests/manifest.test.js` asserts the
|
||||
exact token set in both manifests, so dropping `'wasm-unsafe-eval'` (a silent
|
||||
20x regression on the key derivation) and adding anything beyond it both fail
|
||||
The policy is pinned in both directions. `tests/manifest.test.js` asserts the
|
||||
exact directive set and the exact token set of each directive in both manifests,
|
||||
so dropping `'wasm-unsafe-eval'` (a silent 20x regression on the key
|
||||
derivation), dropping `default-src`, and adding anything anywhere all fail
|
||||
`make check`. `tests/vaultBackend.test.js` asserts the unit tests run the WASM
|
||||
backend, and `make test-e2e` compiles a WebAssembly module inside the real popup
|
||||
under the real manifest.
|
||||
|
||||
Reference in New Issue
Block a user