From 73fc21b6a84effa487f4f1fc0c1c21832eb6660e Mon Sep 17 00:00:00 2001 From: sneak Date: Mon, 21 Sep 2026 18:16:20 +0000 Subject: [PATCH 1/2] test: assert HSTS, CSP, and Permissions-Policy headers Failing test for the three security headers required before 1.0 (issue #91). Implementation follows in the next commit. Model: opus-4-8 --- .../middleware/middleware_internal_test.go | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/internal/middleware/middleware_internal_test.go b/internal/middleware/middleware_internal_test.go index ac3f128..9992a60 100644 --- a/internal/middleware/middleware_internal_test.go +++ b/internal/middleware/middleware_internal_test.go @@ -56,6 +56,61 @@ func TestSecurityHeaders(t *testing.T) { } } +func TestSecurityHeaders_PolicyHeaders(t *testing.T) { + t.Parallel() + + cfg := &config.Config{} + mw := &Middleware{ + log: slog.Default(), + config: cfg, + } + + testHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + handler := mw.SecurityHeaders()(testHandler) + + req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/test", nil) + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + tests := []struct { + header string + want string + }{ + {"Strict-Transport-Security", "max-age=31536000; includeSubDomains"}, + { + "Content-Security-Policy", + "default-src 'self'; " + + "script-src 'self' 'unsafe-inline'; " + + "style-src 'self' 'unsafe-inline'; " + + "object-src 'none'; " + + "base-uri 'self'; " + + "form-action 'self'; " + + "frame-ancestors 'none'", + }, + { + "Permissions-Policy", + "accelerometer=(), autoplay=(), camera=(), " + + "display-capture=(), geolocation=(), gyroscope=(), " + + "magnetometer=(), microphone=(), payment=(), usb=()", + }, + } + + for _, tt := range tests { + t.Run(tt.header, func(t *testing.T) { + t.Parallel() + + got := rec.Header().Get(tt.header) + if got != tt.want { + t.Errorf("%s = %q, want %q", tt.header, got, tt.want) + } + }) + } +} + func TestSecurityHeaders_PreservesExistingHeaders(t *testing.T) { t.Parallel() -- 2.54.0 From 3afce4838349b3ef8eadac8623d83abc2139b9b9 Mon Sep 17 00:00:00 2001 From: sneak Date: Mon, 21 Sep 2026 18:16:20 +0000 Subject: [PATCH 2/2] feat: add HSTS, CSP, and Permissions-Policy security headers (closes #91) SecurityHeaders() now also sets Strict-Transport-Security, Content-Security-Policy, and Permissions-Policy. HSTS is emitted unconditionally: browsers ignore it over plaintext (RFC 6797 section 8.1), so it never lies about the connection and no forwarded-proto header need be trusted. The CSP baseline is default-src 'self' with frame-ancestors 'none' as the primary clickjacking control. script-src and style-src carry 'unsafe-inline' because the generator template has inline onclick handlers and the bundled Tailwind asset injects a runtime