Skip to content

Add a strict Content-Security-Policy app-wide - #2

Merged
ajaydsouza merged 1 commit into
masterfrom
wz/elegant-dijkstra-lvdz1o
Oct 20, 2026
Merged

ajaydsouza merged 1 commit into
masterfrom
wz/elegant-dijkstra-lvdz1o

Conversation

@ajaydsouza

Copy link
Copy Markdown
Contributor

This adds a second lock behind the HTML sanitiser. The reader prints third-party article markup unescaped. With this policy, even markup that slipped past the sanitiser couldn't run script.

The policy

ContentSecurityPolicy middleware applies to every web page, including the installer:

default-src 'none'; script-src 'self' 'nonce-…'; style-src 'self' https://fonts.bunny.net;
font-src 'self' https://fonts.bunny.net; img-src 'self' https: data:; connect-src 'self';
manifest-src 'self'; worker-src 'self'; form-action 'self'; frame-ancestors 'self'; base-uri 'none'
  • Scripts: there's no unsafe-inline or unsafe-eval. The nonce is fresh per request and comes from Vite::useCspNonce(), which also stamps it on the built asset tags.
  • Images: any https: host is allowed, because article images that weren't downloaded keep their remote src.
  • Stored article images keep their own default-src 'none'.
  • npm run dev: while the Vite dev server runs, its origin, its websocket and inline styles are allowed.

What had to change to live under it

  • Alpine is now @alpinejs/csp. reader and the new bulkSelection are registered components that read their settings from data-* attributes; the views no longer build a config object in the markup.
  • Inline handlers are gone. All onclick, onsubmit and onchange attributes are replaced by data-* attributes, handled by one listener in resources/js/behaviours.js:
    • data-confirm asks before submitting a form;
    • data-autosubmit submits a form when its select changes;
    • data-select-on-click selects a field's text;
    • data-bookmarklet-hint shows the "drag me" message instead of following the link.
  • Inline scripts carry the nonce: the reader's pre-paint theme script, the service-worker registration, the bookmarklet popup's script and the installer's <style> block.
  • No style="" attributes: the reader's themed borders and swatches are CSS classes now.
  • Unused Breeze components removed: modal, dropdown and dropdown-link. The modal couldn't work under the CSP build.
  • The README's security notes describe the policy and say where new behaviour should go.

Testing

  • New ContentSecurityPolicyTest checks:

    • the directives themselves;
    • that the nonce is fresh per request and on every <script>;
    • that no page uses an inline handler or style attribute (it renders every page, the bookmarklet popup and the login screen);
    • the installer's <style> nonce;
    • that image responses keep their own policy;
    • the dev-server allowance.
  • Checked in headless Chromium against the built bundle under this exact policy, before pushing:

    • select-all, the bulk action bar, the delete confirmation and the tag input;
    • tag rename toggle, data-confirm, sort autosubmit, the bookmarklet hint and select-on-click;
    • the reader's panel, text size, theme and typeface, remembered across reloads, and scroll progress posting.

    None of the app's own code triggered a CSP violation. An injected onclick, onerror, <script> and javascript: link were all blocked.


Generated by Claude Code

A second lock behind the HTML sanitiser for the reader, which prints
third-party article markup. Every web page, the installer included, now
sends a policy that allows scripts only from this origin or with a
per-request nonce: no unsafe-inline, no unsafe-eval, no inline event
handlers, javascript: URLs or style attributes, nothing framing the app
and forms posting only to it. Stored article images keep their own
tighter policy, and the Vite dev server is allowed while it runs.

To work under it, Alpine is now its CSP build: the reader and bulk
selection are registered components that read their config from data
attributes, and the inline onclick/onsubmit/onchange attributes are
replaced by data-* hooks handled in resources/js/behaviours.js. The
unused Breeze modal and dropdown components are removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014DPvSBmPyHN2QVmXsQQVKe
@ajaydsouza
ajaydsouza merged commit 76c152e into master Oct 20, 2026
1 check passed
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