CSP tripwire: source-scan test + AGENTS.md rule; drop last native onchange

templates/csp_source_test.go scans .templ sources and fails the build on
the three authoring patterns hash-mode CSP refuses silently: native on*=
handler attributes (quote and templ brace forms), inline <script>
content outside the sanctioned theme_flash_guard.templ, and htmx's
eval-dependent bindings (hx-on*, js:-prefixed hx-vals — 'unsafe-eval' is
not granted). Verified to fire on all four violation classes via a
scratch probe file.

The dry run immediately caught a live leftover: unlinked_books.templ's
onchange="updateSelectedCount()" — CSP-refused since the hash flip.
Removed; the count already updates through the document-level delegated
change listener that x-init="setupEventDelegation()" registers.

AGENTS.md gains a "CSP discipline" ALWAYS-apply section documenting the
hash allow-list, the sanctioned-guard invariant, the replacement
patterns, and the tripwire test.

Verified: go build/vet, templates+middleware+docs tests green; /unlinked-
books loads with zero console violations (no fixture rows to exercise the
checkbox; the delegated path is unchanged code).
This commit is contained in:
John O'Keefe committed 2026-10-26 08:17:49 -04:00
1 parent 56dbca37d0
commit 4681e98766
4 files changed
+119 -2

No files matched your search

+32
View File
@@ -41,6 +41,38 @@ with heavy interactivity. First-paint section markup is still templ
deliberate; do not clone it to new pages, and do not "fix" the dashboard
without restoring a templ-partial fade path first.
## CSP discipline (hash-allow-listed script-src) — ALWAYS apply
script-src carries a sha256 hash, not `'unsafe-inline'` (and never
`'unsafe-eval'` — the Alpine build is `@alpinejs/csp` for exactly this
reason; keep it in lockstep with alpinejs). The browser therefore
refuses, **silently at build/test time**, two authoring patterns:
- **No inline `<script>` content outside `theme_flash_guard.templ`.**
The guard is the app's only inline script; its bytes are pinned by
`middleware.ScriptHashThemeFlashGuard` + the drift test. Editing the
guard means recomputing the hash (the failing test prints it); adding
a *second* inline script means hashing it too and extending
`cspSanctionedInlineScripts` in `templates/csp_source_test.go`.
Everything else goes in a bundled `web/src` module.
- **No native `on*=` handler attributes** (`onclick=`, `onerror=`,
`onsubmit=`, `onchange=`, … — quote AND templ brace forms). Inline
handlers never go through the hash path; hash-mode CSP refuses them
unconditionally. Use Alpine `@click`/`@change`/… bound to component
methods (see the 2026-10 CSP-build conversion commits for the
pattern), or delegated listeners for htmx-swapped content.
- **No htmx eval-dependent bindings** (`hx-on*`, `js:`-prefixed
`hx-vals`) — `'unsafe-eval'` is not granted.
`templates/csp_source_test.go` is the tripwire for all three (it
scans the `.templ` sources; it runs in seconds on host). `style-src
'unsafe-inline'` remains by design (htmx-injected styles, Alpine
`:style`); `'self'` still trusts the origin not to serve
attacker-supplied scripts — don't let upload paths become
script-executable. Remember: `templ` does not interpolate `{ expr }`
inside quoted attribute values (gotcha #1 below), so dynamic
`@click={ "fn('" + id + "')" }` attribute values must BE expressions.
## Library visibility (family-lens rule) — ALWAYS apply
Per-user library visibility (`library_visibility` table, default-ALLOW: