From 5ea5356c1d3a5d2dfa5a5d114ffcb6eb3ef7773a Mon Sep 17 00:00:00 2001 From: sneak Date: Thu, 15 Oct 2026 09:07:01 +0000 Subject: [PATCH] Drop 'unsafe-inline' from the Content-Security-Policy (closes #530) The delete prompts on the webhook page were inline onsubmit handlers. Each delete form now carries its question in a data-confirm attribute, which one submit listener in static/js/app.js asks. The three style attributes became Tailwind classes, with tailwind.css regenerated, and the x-cloak rule moved to style.css. The policy now sends script-src 'self' and style-src 'self'. The browser test now answers a target's delete prompt no and then yes, and also loads the sign-in page, the webhook list, the settings and profile pages and the 404 error page under the policy. The README states the policy as sent. Model: opus-5-5 --- README.md | 78 ++++++++++------- .../handlers/delivery_result_view_test.go | 3 +- internal/handlers/event_body.go | 9 +- internal/handlers/event_detail_test.go | 2 +- internal/handlers/source_detail_test.go | 35 ++++---- internal/middleware/middleware.go | 8 +- internal/middleware/middleware_test.go | 29 +++++-- internal/server/alpine_browser_test.go | 85 ++++++++++++++++++- static/css/style.css | 14 ++- static/css/tailwind.css | 2 +- static/js/app.js | 12 +++ templates/event_body.html | 2 +- templates/htmlheader.html | 1 - templates/settings.html | 8 +- templates/source_detail.html | 25 +++--- 15 files changed, 219 insertions(+), 94 deletions(-) diff --git a/README.md b/README.md index 4f2520a..0a0fc1c 100644 --- a/README.md +++ b/README.md @@ -1251,34 +1251,37 @@ markup. The CSP build runs no expressions, so every Alpine directive in `static/js/app.js`: `x-data="collapsible"` and `@click="toggle"`, never `x-data="{ open: false }"` or `@click="open = !open"`. -A browser test in `internal/server` loads the webhook page, its edit pages and -the event log under the real policy and checks that: the add entrypoint form -stays hidden until Add is clicked; for every target type, the targets section's -Add shows only a choice of type with Next and Cancel, Next shows only that -type's fields (no url field for `database` or `log`), Cancel at either step -closes the form, and saving adds the target; a refused target comes back with -its form open, the values entered and the reason, and after Cancel the next Add -starts with an empty form and no reason; a refused save on the target edit page -and on the webhook edit page comes back with the reason and every value entered; -the Copy button beside an entrypoint URL reads "Copied" once clicked; an -entrypoint's Edit button shows its edit form in place of its description and -hides until the form closes, Cancel hides the form and drops what was typed, as -does leaving the page and going back to it, and Save changes the description; of -the recent events on the webhook page only the newest starts expanded, each -expands and collapses, and Open leads to the event's own page; of the events in -the event log only the newest starts expanded, and an event there expands and -collapses when its row's caret or its ID is clicked, and from the keyboard, but -not when its ID is selected with the mouse, and a delivery's attempts inside it -expand and collapse; and at phone width the menu button opens and closes the -mobile menu, and neither the webhook page nor the event log, with a delivery's -attempts open, scrolls sideways or cuts anything off at the page's or a card's -edge. It also fails if the browser reports a console warning or error, an -uncaught exception, or anything the policy refused. `make check` and the image -build lint it but do not run it, and `make test` leaves it out (its file is -built only with the `browser` build tag). Run it with `make test-browser` after -changing `templates/` or `static/js/`: that builds `Dockerfile.browser`, which -runs the test in a digest-pinned headless browser image, so the host needs no -browser. +A browser test in `internal/server` loads the sign-in page, the webhook page, +its edit pages, the event log, the webhook list, the settings page, the profile +page and the `404` error page shown for a path that does not exist, under the +real policy and checks that: the add entrypoint form stays hidden until Add is +clicked; for every target type, the targets section's Add shows only a choice of +type with Next and Cancel, Next shows only that type's fields (no url field for +`database` or `log`), Cancel at either step closes the form, and saving adds the +target; a refused target comes back with its form open, the values entered and +the reason, and after Cancel the next Add starts with an empty form and no +reason; a refused save on the target edit page and on the webhook edit page +comes back with the reason and every value entered; the Copy button beside an +entrypoint URL reads "Copied" once clicked; an entrypoint's Edit button shows +its edit form in place of its description and hides until the form closes, +Cancel hides the form and drops what was typed, as does leaving the page and +going back to it, and Save changes the description; of the recent events on the +webhook page only the newest starts expanded, each expands and collapses, and +Open leads to the event's own page; of the events in the event log only the +newest starts expanded, and an event there expands and collapses when its row's +caret or its ID is clicked, and from the keyboard, but not when its ID is +selected with the mouse, and a delivery's attempts inside it expand and +collapse; at phone width the menu button opens and closes the mobile menu, and +neither the webhook page nor the event log, with a delivery's attempts open, +scrolls sideways or cuts anything off at the page's or a card's edge; and a +target's Delete asks first, naming the target, sends nothing when the answer is +no and deletes the target when it is yes. It also fails if the browser reports a +console warning or error, an uncaught exception, or anything the policy refused. +`make check` and the image build lint it but do not run it, and `make test` +leaves it out (its file is built only with the `browser` build tag). Run it with +`make test-browser` after changing `templates/` or `static/js/`: that builds +`Dockerfile.browser`, which runs the test in a digest-pinned headless browser +image, so the host needs no browser. The package's tarball is committed as `3p/alpinejs-csp-3.14.9.tgz`, byte for byte as the npm registry publishes it. It is a dependency, not this repo's build @@ -1311,7 +1314,9 @@ and commit the regenerated file with the change. Tailwind takes classes only from the files that `input.css` names in its `@source` lines; a class written in any other file is not generated until that file is named there too. `make check` and the image build fail when the committed file differs from what `make css` -generates. `static/css/style.css` is hand-written and is not generated. +generates. `static/css/style.css` is hand-written and is not generated. The +pages' Content-Security-Policy refuses inline styles, so a template styles an +element with a class, never a `style` attribute. `make css` runs the Tailwind standalone CLI in Docker, at the version and sha256 pinned in the Dockerfile's stylesheet stages; it is never installed on the host. @@ -3106,8 +3111,8 @@ webhooker/ │ ├── static.go # //go:embed directive │ ├── css/input.css # Tailwind input, source for tailwind.css (make css) │ ├── css/tailwind.css # Generated stylesheet the pages load -│ ├── css/style.css # Hand-written, loaded after tailwind.css: btn-small, the pointer cursor for input.css's buttons, the webhook list cards' focus outline -│ ├── js/app.js # Copy-to-clipboard, and the Alpine.js components +│ ├── css/style.css # Hand-written, loaded after tailwind.css: btn-small, the pointer cursor for input.css's buttons, the webhook list cards' focus outline, the rule hiding x-cloak elements +│ ├── js/app.js # Copy-to-clipboard, the delete prompts, and the Alpine.js components │ └── js/alpine.min.js # Alpine.js CSP build, extracted from 3p/ by make assets, not committed ├── templates/ # Go HTML templates (base, login, sources, etc.) ├── script/ # Scripts to Rule Them All entrypoints @@ -3161,8 +3166,9 @@ Applied to all routes in this order: 1. **RequestID** — Generate unique request IDs (chi built-in) 2. **SecurityHeaders** — Production security headers on every response (HSTS - unless `HSTS_MAX_AGE` is `0`, X-Content-Type-Options, X-Frame-Options, CSP, - Referrer-Policy, Permissions-Policy) + unless `HSTS_MAX_AGE` is `0`, X-Content-Type-Options, X-Frame-Options, CSP + `default-src 'self'; script-src 'self'; style-src 'self'`, Referrer-Policy, + Permissions-Policy) 3. **Logging** — Structured request logging (method, URL, status, latency, remote IP, client IP, user agent, request ID) 4. **Metrics** — Prometheus HTTP metrics (if `METRICS_USERNAME` and @@ -3247,6 +3253,12 @@ credential check, see [The login endpoint](#the-login-endpoint). - Production security headers on all responses: HSTS (unless `HSTS_MAX_AGE` is `0`), X-Content-Type-Options (`nosniff`), X-Frame-Options (`DENY`), Content-Security-Policy, Referrer-Policy, and Permissions-Policy +- The Content-Security-Policy is + `default-src 'self'; script-src 'self'; style-src 'self'`: a page takes + scripts and styles only from files webhooker serves, and the browser runs no + inline script or event handler and ignores any ` {{block "head" .}}{{end}} {{end}} diff --git a/templates/settings.html b/templates/settings.html index e78cad3..4c66162 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -13,13 +13,11 @@
+ between its entries. wrap-anywhere breaks inside a + value only when it alone is wider than the line. -->
{{.Name}} - {{.Value}} + {{.Value}}

{{.Description}}

diff --git a/templates/source_detail.html b/templates/source_detail.html index 7e19e35..ecf3fc5 100644 --- a/templates/source_detail.html +++ b/templates/source_detail.html @@ -5,12 +5,10 @@ {{define "content"}} -
+ one line. wrap-anywhere goes only on names and descriptions: a + row too wide for a phone must still run past the edge, where the + browser test sees it, rather than break its controls mid-word. --> +
← Back to webhooks
@@ -23,11 +21,12 @@
Full Event Log Edit - -
+ +
@@ -90,7 +89,7 @@ {{if .Active}}Deactivate{{else}}Activate{{end}} -
+
@@ -278,7 +277,7 @@ {{if .Active}}Deactivate{{else}}Activate{{end}} -
+