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:
John O'Keefe committed 2026-10-25 20:54:09 -04:00
1 parent 6a598786e9
commit fb95b6d400
26 files changed
+180 -54

No files matched your search

+13 -13
View File
@@ -29,7 +29,7 @@ templ AdminLibrary(user User, libraries []LibraryData, users []User, archivedCou
<p class="text-sm" style="color: var(--text-secondary)">Manage media libraries, folders, and scanning</p>
</div>
<button
onclick="document.getElementById('create-library-modal').classList.remove('hidden')"
@click="openCreateLibraryModal()"
class="btn btn-primary"
>
@Icon("plus", "h-4 w-4")
@@ -52,7 +52,7 @@ templ AdminLibrary(user User, libraries []LibraryData, users []User, archivedCou
</div>
<div class="flex items-center gap-2">
<a href="/admin/archived" class="btn btn-secondary">View Archived</a>
<button type="button" onclick="purgeArchivedItems()" class="btn btn-secondary">
<button type="button" @click="purgeArchivedItems()" class="btn btn-secondary">
@Icon("trash", "h-4 w-4")
Purge Archived Now
</button>
@@ -69,7 +69,7 @@ templ AdminLibrary(user User, libraries []LibraryData, users []User, archivedCou
<div class="card p-6 w-full max-w-md" style="box-shadow: var(--shadow-pop);">
<div class="flex justify-between items-center mb-6">
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Create Library</h2>
<button type="button" onclick="document.getElementById('create-library-modal').classList.add('hidden')" class="icon-btn" aria-label="Close">
<button type="button" @click="hideModal('create-library-modal')" class="icon-btn" aria-label="Close">
@Icon("close", "h-5 w-5")
</button>
</div>
@@ -77,7 +77,7 @@ templ AdminLibrary(user User, libraries []LibraryData, users []User, archivedCou
hx-post="/admin/library/create"
hx-target="#libraries-container"
hx-swap="innerHTML"
onsubmit="document.getElementById('create-library-modal').classList.add('hidden')"
@submit="hideModal('create-library-modal')"
>
<div class="mb-4">
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Library Name</label>
@@ -94,7 +94,7 @@ templ AdminLibrary(user User, libraries []LibraryData, users []User, archivedCou
</select>
</div>
<div class="flex justify-end space-x-3">
<button type="button" onclick="document.getElementById('create-library-modal').classList.add('hidden')" class="btn btn-secondary">Cancel</button>
<button type="button" @click="hideModal('create-library-modal')" class="btn btn-secondary">Cancel</button>
<button type="submit" class="btn btn-primary">
@Icon("plus", "h-4 w-4")
Create
@@ -108,11 +108,11 @@ templ AdminLibrary(user User, libraries []LibraryData, users []User, archivedCou
<div class="card p-6 w-full max-w-md" style="box-shadow: var(--shadow-pop);">
<div class="flex justify-between items-center mb-6">
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Edit Library</h2>
<button type="button" onclick="document.getElementById('edit-library-modal').classList.add('hidden')" class="icon-btn" aria-label="Close">
<button type="button" @click="hideModal('edit-library-modal')" class="icon-btn" aria-label="Close">
@Icon("close", "h-5 w-5")
</button>
</div>
<form id="edit-library-form" hx-target="#libraries-container" hx-swap="innerHTML" onsubmit="document.getElementById('edit-library-modal').classList.add('hidden')">
<form id="edit-library-form" hx-target="#libraries-container" hx-swap="innerHTML" @submit="hideModal('edit-library-modal')">
<div class="mb-4">
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Library Name</label>
<input type="text" name="name" id="edit-library-name" class="input" required/>
@@ -122,7 +122,7 @@ templ AdminLibrary(user User, libraries []LibraryData, users []User, archivedCou
<textarea name="description" id="edit-library-desc" rows="3" class="input"></textarea>
</div>
<div class="flex justify-end space-x-3">
<button type="button" onclick="document.getElementById('edit-library-modal').classList.add('hidden')" class="btn btn-secondary">Cancel</button>
<button type="button" @click="hideModal('edit-library-modal')" class="btn btn-secondary">Cancel</button>
<button type="submit" class="btn btn-primary">
@Icon("save", "h-4 w-4")
Save
@@ -136,7 +136,7 @@ templ AdminLibrary(user User, libraries []LibraryData, users []User, archivedCou
<div class="card p-6 w-full max-w-lg mx-4" style="box-shadow: var(--shadow-pop);">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Browse Folders</h2>
<button type="button" onclick="document.getElementById('folder-browser-modal').classList.add('hidden')" class="icon-btn" aria-label="Close">
<button type="button" @click="hideModal('folder-browser-modal')" class="icon-btn" aria-label="Close">
@Icon("close", "h-5 w-5")
</button>
</div>
@@ -148,7 +148,7 @@ templ AdminLibrary(user User, libraries []LibraryData, users []User, archivedCou
<div class="card p-6 w-full max-w-md mx-4" style="box-shadow: var(--shadow-pop);">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Delete Library</h2>
<button type="button" onclick="document.getElementById('delete-library-modal').classList.add('hidden')" class="icon-btn" aria-label="Close">
<button type="button" @click="hideModal('delete-library-modal')" class="icon-btn" aria-label="Close">
@Icon("close", "h-5 w-5")
</button>
</div>
@@ -165,7 +165,7 @@ templ AdminLibrary(user User, libraries []LibraryData, users []User, archivedCou
</li>
</ul>
<div class="flex justify-end space-x-3">
<button type="button" onclick="document.getElementById('delete-library-modal').classList.add('hidden')" class="btn btn-secondary">Cancel</button>
<button type="button" @click="hideModal('delete-library-modal')" class="btn btn-secondary">Cancel</button>
<button type="button" id="delete-library-confirm" class="btn btn-danger">
@Icon("trash", "h-4 w-4")
Delete
@@ -195,7 +195,7 @@ templ LibraryList(user User, libraries []LibraryData, users []User) {
</span>
<h3 class="text-xl font-semibold mb-2" style="color: var(--text-primary)">No Libraries Yet</h3>
<p class="mb-4 text-sm" style="color: var(--text-secondary)">Create your first library to get started</p>
<button onclick="document.getElementById('create-library-modal').classList.remove('hidden')" class="btn btn-primary">Create Your First Library</button>
<button @click="openCreateLibraryModal()" class="btn btn-primary">Create Your First Library</button>
</div>
} else {
<div class="space-y-4">
@@ -411,7 +411,7 @@ templ FolderBrowserContent(currentPath string, parentPath string, entries []DirE
type="button"
data-target-input={ targetInput }
data-current-path={ currentPath }
onclick="document.getElementById(this.dataset.targetInput).value = this.dataset.currentPath; document.getElementById('folder-browser-modal').classList.add('hidden')"
@click="pickBrowsePath($event)"
class="btn btn-primary text-sm"
>
@Icon("check", "h-4 w-4")