CSP: replace 'unsafe-inline' with a content hash for the theme flash guard
script-src now allow-lists the guard by sha256 of its rendered bytes (middleware.ScriptHashThemeFlashGuard) instead of 'unsafe-inline' — the guard is the app's only inline <script>. security_headers_test.go renders templates.ThemeFlashGuard() and fails with the replacement hash if the script text ever drifts. Executing anti-XSS strict CSP surfaced three latent violations the eval-removal sweep couldn't see (hash mode is stricter than eval mode, because inline event handler ATTRIBUTES now count against script-src): 1. admin_archived / admin_library: 15 native onclick=/onsubmit= handlers (modal open/close, folder-browser picker, purge button) converted to Alpine @click/@submit component methods — openCreateLibraryModal, hideModal, pickBrowsePath added to the admin component; purgeArchivedItems promoted from its window global into the component and the (window as any) exposure removed. 2. dashboard/docs: onclick handlers converted — dashboard's event.stopPropagation() → @click.stop; docs' sidebar/search closers → closeDocsSidebar/closeSearchResults component methods. 3. 8 onerror= image-fallback attributes (cover placeholders app-wide + audiobook cover hide) replaced by data-fallback attributes handled by one delegated capture-phase listener in web/src/image_fallback.ts — works for htmx-swapped content without re-binding. Verified: theme guard applies a saved theme on public pages under the hash CSP; 14-page authenticated sweep clean; admin pages clean under the promoted admin; admin-library modal open/close bindings exercised; readers render error-free; go build/vet/middleware-test, tsc, vp check/test (146), build:ts all green.
This commit is contained in:
1 parent
6a598786e9
commit
fb95b6d400
26 files changed
+180
-54
No files matched your search
@@ -4,6 +4,11 @@ import (
|
||||
"github.com/labstack/echo/v5"
|
||||
)
|
||||
|
||||
// ScriptHashThemeFlashGuard is the CSP script-src hash of the theme flash
|
||||
// guard's inline script (the app's only inline script). Enforced against
|
||||
// the rendered component by security_headers_test.go.
|
||||
const ScriptHashThemeFlashGuard = "'sha256-JYIVpFyKdLUtWizMha0v69f6yHds+GbJRBWjvpuECBY='"
|
||||
|
||||
// SecurityHeadersMiddleware adds security headers to all responses
|
||||
func SecurityHeadersMiddleware() echo.MiddlewareFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
@@ -14,12 +19,23 @@ func SecurityHeadersMiddleware() echo.MiddlewareFunc {
|
||||
c.Response().Header().Set("X-XSS-Protection", "1; mode=block")
|
||||
c.Response().Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload")
|
||||
// script-src notes: 'unsafe-inline' is required by the pre-paint
|
||||
// theme flash guard (inline <script>). Alpine runs via its
|
||||
// CSP-safe build (@alpinejs/csp), which needs no eval — do NOT
|
||||
// CSP-safe Alpine build (@alpinejs/csp) needs no eval — do NOT
|
||||
// add 'unsafe-eval' here without removing that build.
|
||||
//
|
||||
// The inline script allow-list: the theme flash guard is the
|
||||
// ONLY inline script in the app, and its byte-stable content is
|
||||
// hashed into script-src (sha256 of the bytes between
|
||||
// <script> and </script> as rendered by
|
||||
// templates.ThemeFlashGuard()). Editing the guard requires
|
||||
// recomputing the hash — enforced by
|
||||
// internal/middleware/security_headers_test.go, which renders
|
||||
// the component and asserts the hash matches.
|
||||
//
|
||||
// style-src keeps 'unsafe-inline' for htmx-injected styles and
|
||||
// Alpine :style attribute writes.
|
||||
// font/img blob: — the reader renders EPUB-embedded fonts and
|
||||
// images from blob: URLs inside the foliate content frame.
|
||||
c.Response().Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https: blob:; font-src 'self' data: blob:; frame-src 'self' blob:")
|
||||
c.Response().Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self' "+ScriptHashThemeFlashGuard+"; style-src 'self' 'unsafe-inline'; img-src 'self' data: https: blob:; font-src 'self' data: blob:; frame-src 'self' blob:")
|
||||
c.Response().Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
|
||||
c.Response().Header().Set("Permissions-Policy", "geolocation=(), microphone=(), camera=()")
|
||||
|
||||
|
||||
Reference in new issue
Block a user