From c75e942da8e66bc6d93be8656cc2aefc162fd47b Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Sun, 4 Oct 2026 13:00:12 +0000 Subject: [PATCH 1/2] Expect a Content-Security-Policy without unsafe-inline The security headers test now expects script-src and style-src to allow only 'self', and checks that the policy carries no 'unsafe-inline' at all. It fails until the login and generator pages stop needing inline script and style. Model: opus-5-5 --- internal/middleware/middleware_internal_test.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/internal/middleware/middleware_internal_test.go b/internal/middleware/middleware_internal_test.go index d57d081..91ba3f3 100644 --- a/internal/middleware/middleware_internal_test.go +++ b/internal/middleware/middleware_internal_test.go @@ -325,6 +325,13 @@ func TestSecurityHeaders_PolicyHeaders(t *testing.T) { handler.ServeHTTP(rec, req) + // The login and generator pages load their script and stylesheet from + // /static, so the policy allows no inline script or style. + csp := rec.Header().Get("Content-Security-Policy") + if strings.Contains(csp, "unsafe-inline") { + t.Errorf("Content-Security-Policy allows unsafe-inline: %q", csp) + } + tests := []struct { header string want string @@ -333,8 +340,8 @@ func TestSecurityHeaders_PolicyHeaders(t *testing.T) { { "Content-Security-Policy", "default-src 'self'; " + - "script-src 'self' 'unsafe-inline'; " + - "style-src 'self' 'unsafe-inline'; " + + "script-src 'self'; " + + "style-src 'self'; " + "object-src 'none'; " + "base-uri 'self'; " + "form-action 'self'; " + -- 2.54.0 From 3d008b3017079c81bf03dd3e979b5e6eb21c30d8 Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Sun, 4 Oct 2026 13:02:39 +0000 Subject: [PATCH 2/2] Remove unsafe-inline from the Content-Security-Policy (closes #125) script-src and style-src now allow only 'self'. The generator page's two inline onclick handlers, which selected the generated URL and copied it, move into internal/static/generator.js and are attached with addEventListener. The bundled Tailwind script, which built styles in the browser and injected them at runtime, is replaced by a small hand-written internal/static/style.css holding only the rules the login and generator pages use; the templates carry a few plain class names in place of Tailwind's. No build step. The pages keep their layout, not every pixel of it. Model: opus-5-5 --- README.md | 5 +- TODO.md | 9 ++ internal/middleware/middleware.go | 9 +- internal/server/routes.go | 2 +- internal/static/generator.js | 10 ++ internal/static/static.go | 2 +- internal/static/style.css | 190 ++++++++++++++++++++++++++++++ internal/static/tailwind.js | 83 ------------- internal/templates/generator.html | 88 +++++--------- internal/templates/login.html | 20 ++-- 10 files changed, 256 insertions(+), 162 deletions(-) create mode 100644 internal/static/generator.js create mode 100644 internal/static/style.css delete mode 100644 internal/static/tailwind.js diff --git a/README.md b/README.md index 6e3e2d1..fe05438 100644 --- a/README.md +++ b/README.md @@ -191,8 +191,9 @@ path under `/v1/` answers 200, in maintenance mode too. - `GET /.well-known/healthcheck.json` — JSON with `status` (`ok`), `now`, `uptime_seconds`, `uptime_human`, `version`, `appname` and `maintenance_mode`. Needs: nothing. Answers: 200, always. -- `GET /static/` — the script the login and generator pages load. Needs: - nothing. Answers: 200, or 404 for a file that does not exist. +- `GET /static/` — the stylesheet and script the login and generator + pages load. Needs: nothing. Answers: 200, or 404 for a file that does not + exist. - `GET /metrics` — Prometheus metrics (see Architecture). Needs: HTTP basic authentication with `metrics.username` and `metrics.password`. Answers: 200; 401 without them; 404 when they are not set, as the route then does not exist. diff --git a/TODO.md b/TODO.md index 11b5a2d..d4be840 100644 --- a/TODO.md +++ b/TODO.md @@ -58,6 +58,15 @@ P2: security: referer blacklist deprecation warning; its successor `gomodguard_v2` runs with the shared module block list, and `depguard` keeps `net/http/httptest` out of files that are not tests. The tree needed no code changes. +- 2026-10-04 the Content-Security-Policy allows no inline script or style + (closes #125): `script-src` and `style-src` are `'self'` only. The generator + page's two inline `onclick` handlers moved into + `internal/static/generator.js`, attached with `addEventListener`; the bundled + Tailwind script, which built styles in the browser, is replaced by a small + hand-written `internal/static/style.css` with only the rules the login and + generator pages use, the templates carrying a few plain class names in place + of Tailwind's. No build step. The pages keep their layout, not every pixel of + it. - 2026-10-04 deployment guide and example Caddy config (closes #89): "Deployment" in `README.md` says what the reverse proxy in front of pixa must do (terminate TLS; pass `Host`, `Origin` and `Referer` on unchanged; set diff --git a/internal/middleware/middleware.go b/internal/middleware/middleware.go index 358cbdc..aeddfd8 100644 --- a/internal/middleware/middleware.go +++ b/internal/middleware/middleware.go @@ -35,13 +35,10 @@ const HSTSValue = "max-age=31536000; includeSubDomains" // ContentSecurityPolicyValue is the Content-Security-Policy header value. // default-src 'self' is the baseline and frame-ancestors 'none' is the primary -// clickjacking control. 'unsafe-inline' is required in script-src and style-src -// because the served templates carry inline onclick handlers (generator page) -// and the bundled Tailwind asset injects a runtime