harden: drop 'unsafe-inline' from style-src (closes #328)
check / check (push) Failing after 2s
e2e / e2e-chrome (push) Failing after 3s
e2e / e2e-firefox (push) Failing after 2s

The popup's markup no longer carries style attributes. The 42 in
index.html and in the HTML the view helpers build are now Tailwind
classes, each computing to the value it replaced, so style-src is 'self'
in both manifests, pinned in tests/manifest.test.js.

The address dot's 16 colours are written out as whole classes, because
Tailwind builds only the classes it finds in the source. The Settings
debug well is shown and hidden with the hidden class, since clearing an
inline display no longer uncovers it. Two tests that found the colour dot
by its inline style now find it by its class. Script that sets
element.style is unaffected.

Model: opus-5-5
This commit is contained in:
2026-10-05 13:00:03 +00:00
parent a0360a7874
commit f476e77732
13 changed files with 108 additions and 147 deletions
+19 -15
View File
@@ -814,13 +814,15 @@ discoverable.
on critical screens and when space is available to allow users to disambiguate
addresses visually, as a security feature.
- **Tailwind CSS**: Utility-first CSS via Tailwind. No custom CSS classes for
styling. Tailwind is configured with a minimal monochrome palette. This keeps
the styling co-located with the markup and eliminates CSS file management. The
handful of classes in `styles/main.css` are not styling: `.copy-flash-*`
carries the copy feedback animation, and `.am-address` carries the rule that
an address never wraps. Both are invariants that hold in every place they
appear, and spelling either out as repeated utilities is how one of those
places drifts away from the rest.
styling, and no `style="..."` attributes, which the
[Content Security Policy](#content-security-policy) refuses. Tailwind is
configured with a minimal monochrome palette. This keeps the styling
co-located with the markup and eliminates CSS file management. The handful of
classes in `styles/main.css` are not styling: `.copy-flash-*` carries the copy
feedback animation, and `.am-address` carries the rule that an address never
wraps. Both are invariants that hold in every place they appear, and spelling
either out as repeated utilities is how one of those places drifts away from
the rest.
- **Vanilla JS**: No framework (React, Vue, Svelte, etc.). The popup UI is small
enough that vanilla JS with simple view switching is sufficient. A framework
would add bundle size, build complexity, and attack surface for no benefit at
@@ -2213,7 +2215,7 @@ 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:;
style-src 'self'; img-src 'self' data:;
connect-src 'self' https: http:; frame-src 'none'; form-action 'none';
base-uri 'none'
```
@@ -2225,15 +2227,17 @@ 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
`style-src 'self'` admits the stylesheet and nothing inline: both browsers
refuse a `style="..."` attribute and a `<style>` block. So the popup's markup,
in `src/popup/index.html` and in the HTML the view helpers build, carries
Tailwind classes and never a `style` attribute. Script that sets `element.style`
is not affected; that is how the views show and hide their error lines. An
inline style that slips in anyway is refused with a console error, which fails
both end-to-end suites.
These directives differ from a plain `'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