The UI's Alpine.js never runs: the Content-Security-Policy forbids the eval it needs #371

Open
opened 2026-10-01 21:03:30 +02:00 by clawbot · 1 comment
Collaborator

Found while planning #370 and #348.

Every page sends Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' (SecurityHeaders in internal/middleware/middleware.go). The Alpine.js build the UI loads, package/dist/cdn.min.js from alpinejs-3.14.9.tgz, compiles every directive's expression with the async Function constructor, which a policy without 'unsafe-eval' forbids. So in a browser that enforces the policy, no Alpine expression runs: x-cloak is still removed, but x-show, @click and x-data do nothing. That is what the owner saw:

  • #370: the targets section shows the "+ Add" toggle and the always-open add form with its blue button at once, because nothing hides the form.
  • #348: the caret does not collapse an event.

Definition of done:

  • The UI's Alpine directives work in a browser under the policy webhooker sends, and the policy does not gain 'unsafe-eval'. Plan: use Alpine's own CSP build (the @alpinejs/csp npm package at the same version), committed in 3p/ as its tarball and extracted at build time exactly as #345 does for alpinejs, replacing that tarball. The CSP build accepts only simpler expressions, so the pages' directives are rewritten to what it supports, with any component state registered in the committed static/js/app.js.
  • A headless browser test loads the pages with the real headers and fails on the current tree: it shows the add forms hidden until their toggle is clicked, and an event expanding and collapsing.
  • The README's third-party assets section names the package actually used.

Builds on #351 (the vendored tarball). Blocks the UI work in 348, #349, #369 and 370, which all rely on Alpine.

Model: opus-5-5

Found while planning https://git.eeqj.de/sneak/webhooker/issues/370 and https://git.eeqj.de/sneak/webhooker/issues/348. Every page sends `Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'` (`SecurityHeaders` in `internal/middleware/middleware.go`). The Alpine.js build the UI loads, `package/dist/cdn.min.js` from `alpinejs-3.14.9.tgz`, compiles every directive's expression with the async `Function` constructor, which a policy without `'unsafe-eval'` forbids. So in a browser that enforces the policy, no Alpine expression runs: `x-cloak` is still removed, but `x-show`, `@click` and `x-data` do nothing. That is what the owner saw: - https://git.eeqj.de/sneak/webhooker/issues/370: the targets section shows the "+ Add" toggle and the always-open add form with its blue button at once, because nothing hides the form. - https://git.eeqj.de/sneak/webhooker/issues/348: the caret does not collapse an event. Definition of done: - The UI's Alpine directives work in a browser under the policy webhooker sends, and the policy does not gain `'unsafe-eval'`. Plan: use Alpine's own CSP build (the `@alpinejs/csp` npm package at the same version), committed in `3p/` as its tarball and extracted at build time exactly as https://git.eeqj.de/sneak/webhooker/issues/345 does for `alpinejs`, replacing that tarball. The CSP build accepts only simpler expressions, so the pages' directives are rewritten to what it supports, with any component state registered in the committed `static/js/app.js`. - A headless browser test loads the pages with the real headers and fails on the current tree: it shows the add forms hidden until their toggle is clicked, and an event expanding and collapsing. - The README's third-party assets section names the package actually used. Builds on https://git.eeqj.de/sneak/webhooker/pulls/351 (the vendored tarball). Blocks the UI work in 348, https://git.eeqj.de/sneak/webhooker/issues/349, https://git.eeqj.de/sneak/webhooker/issues/369 and 370, which all rely on Alpine. Model: opus-5-5
clawbot self-assigned this 2026-10-01 21:03:30 +02:00
Author
Collaborator

#411 fixes this.

Before the change, a headless Chromium run against next with the real headers showed the defect: the console reported the policy refusing Alpine's evaluation, both add forms showed before Add was clicked, and an event in the event log could not be collapsed.

The UI now loads Alpine's CSP build: the @alpinejs/csp 3.14.9 tarball replaces alpinejs in 3p/, and the policy is unchanged. Every directive in templates/ now names a property or method of a component registered in static/js/app.js: collapsible for everything a click shows and hides, and targetForm for the add target form's per-type fields. A new browser test in internal/server loads the webhook page and the event log under the real headers. It covers the add forms, choosing a target type, expanding and collapsing an event, and a clean console. The README's third-party assets section names the new package.

  • Judgement call: the test skips without chromium on PATH. The Dockerfile's test stage installs Debian's chromium, unpinned like its other packages.
  • Deviation: the type select's x-model became @change, because the 3.14.9 CSP build cannot assign through x-model.
  • New test-only dependency chromedp, which raises golang.org/x/sys to 0.47.0.
  • The registry publishes a sha512 integrity, not a sha256; the tarball matches it.

Model: opus-5-5

https://git.eeqj.de/sneak/webhooker/pulls/411 fixes this. Before the change, a headless Chromium run against `next` with the real headers showed the defect: the console reported the policy refusing Alpine's evaluation, both add forms showed before Add was clicked, and an event in the event log could not be collapsed. The UI now loads Alpine's CSP build: the `@alpinejs/csp` 3.14.9 tarball replaces `alpinejs` in `3p/`, and the policy is unchanged. Every directive in `templates/` now names a property or method of a component registered in `static/js/app.js`: `collapsible` for everything a click shows and hides, and `targetForm` for the add target form's per-type fields. A new browser test in `internal/server` loads the webhook page and the event log under the real headers. It covers the add forms, choosing a target type, expanding and collapsing an event, and a clean console. The README's third-party assets section names the new package. - Judgement call: the test skips without `chromium` on `PATH`. The Dockerfile's test stage installs Debian's `chromium`, unpinned like its other packages. - Deviation: the type select's `x-model` became `@change`, because the 3.14.9 CSP build cannot assign through `x-model`. - New test-only dependency `chromedp`, which raises `golang.org/x/sys` to 0.47.0. - The registry publishes a sha512 integrity, not a sha256; the tarball matches it. Model: opus-5-5
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/webhooker#371