diff --git a/internal/web/securityheaders_test.go b/internal/web/securityheaders_test.go index 8ca47ed..b6d54c7 100644 --- a/internal/web/securityheaders_test.go +++ b/internal/web/securityheaders_test.go @@ -3,7 +3,6 @@ package web import ( "net/http" "net/http/httptest" - "strings" "testing" ) @@ -28,29 +27,20 @@ func TestSecurityHeaders(t *testing.T) { t.Errorf("X-Content-Type-Options = %q, want nosniff", got) } - // JSON response: no security headers. + // JSON/raw responses: headers are now set unconditionally BEFORE the handler + // runs. The previous post-handler approach was silently dropped once a page + // handler flushed its template output (headers must be set before WriteHeader). + // CSP/nosniff/referrer on non-HTML bodies is harmless and desirable. jsonh := SecurityHeaders(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{"ok":true}`)) })) rec = httptest.NewRecorder() jsonh.ServeHTTP(rec, httptest.NewRequest("GET", "/api/x", nil)) - if got := rec.Header().Get("Content-Security-Policy"); got != "" { - t.Errorf("unexpected CSP %q on JSON response", got) + if got := rec.Header().Get("Content-Security-Policy"); got != wantCSP { + t.Errorf("CSP missing on JSON response: got %q", got) } - if got := rec.Header().Get("Referrer-Policy"); got != "" { - t.Errorf("unexpected Referrer-Policy %q on JSON response", got) - } - - // Content type set after the first Write (as the inline can page does) is - // still picked up because headers are inspected post-handler. - lateh := SecurityHeaders(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte("")) - w.Header().Set("Content-Type", "text/html; charset=utf-8") - })) - rec = httptest.NewRecorder() - lateh.ServeHTTP(rec, httptest.NewRequest("GET", "/", nil)) - if got := rec.Header().Get("Content-Security-Policy"); !strings.Contains(got, "frame-ancestors 'none'") { - t.Errorf("CSP = %q, want frame-ancestors 'none'", got) + if got := rec.Header().Get("Referrer-Policy"); got != "no-referrer" { + t.Errorf("Referrer-Policy missing on JSON response: got %q", got) } } diff --git a/internal/web/web.go b/internal/web/web.go index 21e0fa2..51e8b46 100644 --- a/internal/web/web.go +++ b/internal/web/web.go @@ -304,13 +304,15 @@ func (u *UI) Handlers() *Handlers { return &Handlers{UI: u} } // pentest. Runs after the handler so the Content-Type is already set. func SecurityHeaders(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - next.ServeHTTP(w, r) + // Set before the handler runs: once a handler writes (template render + // flushes), header mutations are silently dropped. Setting the headers + // unconditionally is safe: CSP/nosniff/referrer on JSON or /raw bodies + // is harmless and arguably desirable. h := w.Header() - if strings.HasPrefix(h.Get("Content-Type"), "text/html") { - h.Set("Content-Security-Policy", - "default-src 'self'; script-src 'self' 'unsafe-inline'; frame-ancestors 'none'") - h.Set("Referrer-Policy", "no-referrer") - h.Set("X-Content-Type-Options", "nosniff") - } + h.Set("Content-Security-Policy", + "default-src 'self'; script-src 'self' 'unsafe-inline'; frame-ancestors 'none'") + h.Set("Referrer-Policy", "no-referrer") + h.Set("X-Content-Type-Options", "nosniff") + next.ServeHTTP(w, r) }) }