Fix SEC-04: add security headers to API

- Add globalSecurityHeaders middleware wrapping root handler
- Sets X-Content-Type-Options, Strict-Transport-Security, Cache-Control
  on all responses (API and UI)
- Add tests verifying headers on /v1/health and /v1/auth/login

Security: API responses previously lacked HSTS, nosniff, and
cache-control headers. The new middleware applies these universally.
Headers are safe for all content types and do not conflict with
the UI's existing securityHeaders middleware.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 00:41:48 -07:00
parent 586d4e3355
commit 3f09d5eb4f
2 changed files with 69 additions and 1 deletions

View File

@@ -594,3 +594,47 @@ func TestRenewToken(t *testing.T) {
t.Error("old token should be revoked after renewal")
}
}
// TestSecurityHeadersOnAPIResponses verifies that the global security-headers
// middleware (SEC-04) sets X-Content-Type-Options, Strict-Transport-Security,
// and Cache-Control on all API responses, not just the UI.
func TestSecurityHeadersOnAPIResponses(t *testing.T) {
srv, _, _, _ := newTestServer(t)
handler := srv.Handler()
wantHeaders := map[string]string{
"X-Content-Type-Options": "nosniff",
"Strict-Transport-Security": "max-age=63072000; includeSubDomains",
"Cache-Control": "no-store",
}
t.Run("GET /v1/health", func(t *testing.T) {
rr := doRequest(t, handler, "GET", "/v1/health", nil, "")
if rr.Code != http.StatusOK {
t.Fatalf("status = %d, want 200", rr.Code)
}
for header, want := range wantHeaders {
got := rr.Header().Get(header)
if got != want {
t.Errorf("%s = %q, want %q", header, got, want)
}
}
})
t.Run("POST /v1/auth/login", func(t *testing.T) {
createTestHumanAccount(t, srv, "sec04-user")
rr := doRequest(t, handler, "POST", "/v1/auth/login", map[string]string{
"username": "sec04-user",
"password": "testpass123",
}, "")
if rr.Code != http.StatusOK {
t.Fatalf("status = %d, want 200; body: %s", rr.Code, rr.Body.String())
}
for header, want := range wantHeaders {
got := rr.Header().Get(header)
if got != want {
t.Errorf("%s = %q, want %q", header, got, want)
}
}
})
}