Skip to content

feat(security): strict Content Security Policy, no inline script or style - #133

Merged
jarnesjo merged 4 commits into
mainfrom
feat/strict-csp
Nov 2, 2026
Merged

jarnesjo merged 4 commits into
mainfrom
feat/strict-csp

Conversation

@jarnesjo

@jarnesjo jarnesjo commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

Epic 3, the rest of the way: script-src 'self'; style-src 'self' on the dashboard, the shared dashboard and the login page, as the design kit asks. One missed esc() can no longer run script (as it did in v1.26.1); it shows as text.

Four commits, each green on its own:

  1. No inline style attributes. The 127 style="..." in the JS strings and the markup became classes at the end of dashboard.css for fixed values, and data-w (percent), data-h (pixels), data-bg and data-color for bar sizes and per-row colours, applied through the CSSOM by a MutationObserver, which the policy allows. Elements that start hidden use the hidden attribute ([hidden] with !important, since a display rule on the element would show it anyway) and el.hidden = false.
  2. Event delegation. The 83 inline handlers became data-action (click) and data-on-<event> attributes with their arguments in data-*. One listener per event type walks from the target up to the document and runs every action on the way, as the inline handlers bubbled, until one stops propagation. data-on-blur listens to focusout. jsArg() is gone with the handlers. The smoke test's synthetic change events now bubble, as a browser's do.
  3. The policy. The branding and the share reach the page as a JSON data block (<script type="application/json" id="puls-config">, < > & escaped), which script-src does not govern. The theme script in <head> is theme.js, shared with the login page, and applies the install's accent through the CSSOM, so no nonce and no stylesheet endpoint were needed. The login page's styles and CSRF refresh are login.css and login.js. The smoke test registers a securitypolicyviolation listener before every page loads and fails on any violation, so no inline bit can come back.
  4. Docs.

Checked: 360 tests, the smoke test under the strict header, and every README image regenerated and compared by eye (bars, dots, badges, flags, goal meters, legend, tooltips all as before; the images are not committed, they change with the date).

Changelog has a Security entry with the note that anyone upgrading by hand copies all of public/.

🤖 Generated with Claude Code

jarnesjo and others added 4 commits November 2, 2026 11:59
Every style="..." in the dashboard's markup and JS strings is gone, as a
strict Content Security Policy (style-src 'self') requires. Fixed values
became classes at the end of dashboard.css; bar widths and heights and
per-row colours travel as data-w, data-h, data-bg and data-color and are
applied through the CSSOM by a MutationObserver, which the policy allows.
Elements that start hidden use the hidden attribute ([hidden] with
!important, since a display rule on the element would show it anyway)
and are shown with el.hidden = false. No change on screen.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The 83 onclick, oninput, onchange, onsubmit, onkeydown, onpointerdown and
onblur attributes are gone, as a strict Content Security Policy
(script-src 'self') requires: nonces do not cover inline handlers. An
element names its action in data-action (click) or data-on-<event> and
carries its arguments in data attributes; one listener per event type
walks from the target up to the document and runs every action on the
way, as the inline handlers bubbled, until one stops propagation. Blur
does not bubble, so data-on-blur listens to focusout. jsArg() is gone
with the handlers: arguments are escaped as attributes with esc().

The smoke test's synthetic change events now bubble, as a browser's do;
the delegated listener never saw them before.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…tyle

script-src 'self'; style-src 'self' on the dashboard, the shared
dashboard and the login page, as the design kit asks. One missed esc()
can no longer run script (as it did in v1.26.1).

The branding and the share reach the page as a JSON data block
(<script type="application/json" id="puls-config">), which the policy
does not govern; theme.js and dashboard.js read it. The theme script in
<head> is public/theme.js, shared with the login page, and applies the
install's accent through the CSSOM. The login page's styles are
public/login.css and its CSRF refresh public/login.js. The smoke test
fails on any CSP violation (securitypolicyviolation -> console.error).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@jarnesjo
jarnesjo merged commit 4eb0a56 into main Nov 2, 2026
4 checks passed
@jarnesjo
jarnesjo deleted the feat/strict-csp branch November 2, 2026 12:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant