Skip to content

harden: CSP meta + workbox auth-relay tripwire#90

Open
silverbucket wants to merge 5 commits into
masterfrom
t3code/csp-meta-workbox-test
Open

harden: CSP meta + workbox auth-relay tripwire#90
silverbucket wants to merge 5 commits into
masterfrom
t3code/csp-meta-workbox-test

Conversation

@silverbucket
Copy link
Copy Markdown
Owner

Summary

Notes

  • The CSP comment is honest about its scope: it blocks inline-script injection, eval, base-tag hijacking, plugin embeds, cross-origin form posts, and PWA manifest takeover — but it does not close the token-exfiltration channel on its own. connect-src https: has to be wide-open so rs.js can talk to whichever remoteStorage server the user picks. Tightening that further is a deploy-time-header job, called out in the comment.
  • frame-ancestors, report-to, and sandbox are intentionally omitted from the meta CSP — the spec says they're ignored when set via <meta>. Use HTTP headers at deploy for those.
  • The denylist test exercises three realistic OAuth callback shapes (bare path, ?code= query, #access_token= hash) so an over-anchored regex change is caught.

Closes #79.
Closes #80.

Test plan

  • npm test — all 278 existing tests + 2 new workbox tests pass
  • npm run lint — biome clean
  • npm run build — clean build; verified dist/index.html retains the CSP meta tag, dist/auth-relay.html retains its CSP, dist/sw.js precache list does not contain auth-relay.html
  • Manual smoke: load the built app, confirm DevTools console shows no CSP violations, confirm rs.js login still completes through auth-relay.html

Formalises the no-third-party-JS property and limits XSS blast radius
on top of the localStorage-resident remoteStorage tokens. Inline scripts
and styles are pinned by sha256 hash; if either is edited the hash must
be regenerated or the browser refuses to run them.

Honest about its limits: connect-src has to allow https: because rs.js
talks to whichever server the user picks, so this CSP cannot prevent
exfiltration on its own. Closing that gap requires a deploy-time header
that pins connect-src to the discovered host.

Closes #79.
The Workbox precache manifest is generated at build time and a vite-plugin-pwa
bump can silently re-include auth-relay.html, which would break rs.js's
OAuth redirect-back flow (the cached copy intercepts the redirect).

Extracts the workbox config to a named export and asserts both the glob
ignore and the navigate-fallback denylist exclude auth-relay.html. The
denylist assertion exercises the regex against the realistic OAuth
callback shapes (query-param and hash-fragment) so an over-anchored
regex change is caught.

Closes #80.
@silverbucket silverbucket self-assigned this Apr 27, 2026
PR feedback: the previous test asserted the exported config still contained
the auth-relay exclusions, but that wouldn't catch the dependency-bump
failure mode #80 actually cares about — vite-plugin-pwa or Workbox
changing how those options are interpreted at build time.

Replaces the config-level checks with a test that runs vite.build() into
a tempdir and parses the generated sw.js precache list, then drops the
named workboxConfig export since nothing imports it anymore.
PR feedback: it wasn't obvious how the sha256 hashes in the CSP meta tags
get maintained — manual update on every script edit was implicit, not
documented.

Adds csp.test.js, which re-computes sha256 for every inline <script> and
<style> in index.html and public/auth-relay.html and asserts the CSP
meta tag's directive contains it. Editing a script body now fails the
test with the new hash printed in the assertion error, so the dev just
copies it into the meta tag — no manual `node -e` invocation needed.

Trims the over-long CSP comments at the same time.
@silverbucket
Copy link
Copy Markdown
Owner Author

Thanks for the review — addressed all three points in two follow-up commits:

1. Workbox test now inspects the built sw.js (ce08353)
Replaced the config-level assertions with precache.test.js, which runs vite.build() into a tempdir, parses the precache list out of the generated sw.js, and asserts no entry contains auth-relay. Also asserts the navigate-fallback denylist substring survives minification. This actually catches the dep-bump failure mode #80 cares about (a Workbox change that reinterprets globIgnores/navigateFallbackDenylist). Adds ~3s to the test run; keeping it. Dropped the now-unused workboxConfig named export.

2. Hash maintenance is now automatic via csp.test.js (67f3e7b)
The previous setup required node -e "..." to recompute hashes by hand and a comment reminding you to do it. New csp.test.js walks every inline <script> and <style> in index.html and public/auth-relay.html, recomputes sha256, and asserts the CSP meta tag's directive contains it. Edit a script body, the test fails with the new hash printed in the assertion error — paste it into the meta tag, done. Verified by mutating the index.html theme script:

Expected: "'sha256-7xaLG2DD/ajImSSJTPT7Md0M7ACEU0qpci8JoeY+YnQ='"
Received: "script-src 'self' 'sha256-vzZ/YetcF3xkxNnz4tG+fpSoUODEamJeUrlWt4PjMP8='"

3. Trimmed the verbose comments (also in 67f3e7b)
The 25-line CSP block in index.html is now 8 lines; the auth-relay.html comment is 2 lines. Removed the standalone <!-- CSP-HASH: ... --> reminder since the test enforces it.

All 281 tests pass, lint clean, build succeeds.

PR feedback: keeping the hashes pinned in source meant a 30-second
copy-paste from a test failure every time you edited an inline script —
real friction even if rare.

Adds vite-plugin-csp-hash.js, which replaces __CSP_SCRIPT_HASHES__ /
__CSP_STYLE_HASHES__ tokens in HTML CSP meta tags with the sha256 of
every inline <script>/<style> block in the document. Hooks
transformIndexHtml for index.html (dev + build), a configureServer
middleware for public/* HTML in dev, and writeBundle for public/* in
build, so dev refresh and prod build both pick up the new hash with
zero manual step.

Drops csp.test.js — drift is now impossible by construction; the
plugin always derives the hash from the actual content it's hashing.
@silverbucket
Copy link
Copy Markdown
Owner Author

Made it fully automatic — pushed 95f90b8.

Added vite-plugin-csp-hash.js, a small Vite plugin that replaces __CSP_SCRIPT_HASHES__ / __CSP_STYLE_HASHES__ tokens in CSP meta tags with the sha256 of every inline <script>/<style> block in the document. Source HTML now uses placeholders; dev refresh and prod build both auto-fill the correct hash.

Workflow now:

  1. Edit an inline script
  2. Refresh / rebuild
  3. Done

No copy-paste, no test failure to react to. Dropped csp.test.js since drift is impossible by construction — the plugin derives the hash from the actual content it's hashing.

Verified working:

  • Build: dist/index.html and dist/auth-relay.html have the correct sha256 hashes; no __CSP_*__ tokens remain.
  • Dev: curl http://localhost:4173/auth-relay.html returns the rewritten file with hashes filled in (the dev middleware path).
  • Round-trip: mutated var pvar theme_pref in the inline theme script, rebuilt, hash changed from vzZ/Yetc... to ON0l4d7z.... Restored, hash returned to original. Plugin derives from current content.

Plugin scope: ~50 lines, three hooks (transformIndexHtml, configureServer middleware, writeBundle). All 278 tests pass, lint clean.

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.

Verify Workbox precache excludes auth-relay.html Add Content-Security-Policy meta to harden token-in-localStorage

1 participant