Drop 'unsafe-inline' from the Content-Security-Policy (closes #530)
check / check (push) Waiting to run

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
This commit is contained in:
2026-10-15 13:05:41 +00:00
parent 0d38aea12c
commit 5ea5356c1d
15 changed files with 219 additions and 94 deletions
+5 -3
View File
@@ -572,7 +572,9 @@ func (s *Middleware) MetricsAuth() func(http.Handler) http.Handler {
// SecurityHeaders returns middleware that sets production security
// headers on every response: HSTS (unless HSTS_MAX_AGE is zero),
// X-Content-Type-Options, X-Frame-Options, CSP, Referrer-Policy, and
// Permissions-Policy.
// Permissions-Policy. The CSP takes scripts and styles only from
// files on this origin, so a template may hold no inline script,
// event handler or style attribute.
func (s *Middleware) SecurityHeaders() func(http.Handler) http.Handler {
maxAge := s.params.Config.HSTSMaxAge
hsts := "max-age=" + strconv.FormatInt(int64(maxAge/time.Second), 10) +
@@ -594,8 +596,8 @@ func (s *Middleware) SecurityHeaders() func(http.Handler) http.Handler {
w.Header().Set(
"Content-Security-Policy",
"default-src 'self'; "+
"script-src 'self' 'unsafe-inline'; "+
"style-src 'self' 'unsafe-inline'",
"script-src 'self'; "+
"style-src 'self'",
)
w.Header().Set(
"Referrer-Policy",
+23 -6
View File
@@ -747,10 +747,10 @@ func TestNoCache_SetsHeaders(t *testing.T) {
// --- SecurityHeaders Middleware Tests ---
// hstsHeaders serves one request through SecurityHeaders built over
// a Config with the given HSTSMaxAge, and returns every
// Strict-Transport-Security value on the response.
func hstsHeaders(t *testing.T, maxAge time.Duration) []string {
// securityHeaders serves one request through SecurityHeaders built
// over a Config with the given HSTSMaxAge, and returns the response's
// headers.
func securityHeaders(t *testing.T, maxAge time.Duration) http.Header {
t.Helper()
cfg := &config.Config{HSTSMaxAge: maxAge}
@@ -772,7 +772,7 @@ func hstsHeaders(t *testing.T, maxAge time.Duration) []string {
handler.ServeHTTP(w, req)
return w.Header().Values("Strict-Transport-Security")
return w.Header()
}
func TestSecurityHeaders_HSTS(t *testing.T) {
@@ -804,11 +804,28 @@ func TestSecurityHeaders_HSTS(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
assert.Equal(t, tt.expected, hstsHeaders(t, tt.maxAge))
assert.Equal(
t, tt.expected,
securityHeaders(t, tt.maxAge).Values(
"Strict-Transport-Security",
),
)
})
}
}
// TestSecurityHeaders_CSP pins the Content-Security-Policy: a page
// takes its scripts and styles only from files on its own origin,
// never from an inline script, event handler or style.
func TestSecurityHeaders_CSP(t *testing.T) {
t.Parallel()
assert.Equal(
t, "default-src 'self'; script-src 'self'; style-src 'self'",
securityHeaders(t, 0).Get("Content-Security-Policy"),
)
}
// --- MaxBodySize Middleware Tests ---
const testBodyLimit int64 = 64