package main import ( "net/http" "net/http/httptest" "strings" "testing" ) // The baseline policy has to actually permit the frontend Vite builds. These // are the allowances dist/index.html needs; if a future build starts emitting // an inline script or pulling from a new host, that shows up here rather than // as a blank page in production. func TestBaselineCSPCoversTheBuiltFrontend(t *testing.T) { required := []string{ "script-src 'self'", // Vite emits no inline script "'unsafe-inline' https://fonts.googleapis.com", // React style={{…}} + the font link "https://fonts.gstatic.com", // the font files themselves "blob:", // read-aloud plays an object URL "object-src 'none'", "base-uri 'self'", "frame-ancestors 'self'", } for _, want := range required { if !strings.Contains(contentSecurityPolicy, want) { t.Errorf("baseline CSP is missing %q:\n%s", want, contentSecurityPolicy) } } } // The middleware is a floor, not a ceiling: it runs *before* the handler // precisely so a route serving untrusted bytes can overwrite the policy with a // stricter one. This is the contract the image store depends on, and the reason // the policy no longer lives in the Traefik labels — customresponseheaders // would overwrite it in the other direction. func TestRouteMayTightenTheBaselineCSP(t *testing.T) { strict := "default-src 'none'; sandbox" handler := securityHeaders(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Security-Policy", strict) w.WriteHeader(http.StatusOK) })) rec := httptest.NewRecorder() handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/api/images/x.svg", nil)) if got := rec.Header().Get("Content-Security-Policy"); got != strict { t.Fatalf("handler's policy was not honoured: got %q, want %q", got, strict) } // The headers it didn't touch still stand. if rec.Header().Get("X-Content-Type-Options") != "nosniff" { t.Error("baseline nosniff was lost") } } // Every ordinary response carries the baseline. func TestBaselineHeadersOnAPlainResponse(t *testing.T) { handler := securityHeaders(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) })) rec := httptest.NewRecorder() handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/", nil)) for header, want := range map[string]string{ "Content-Security-Policy": contentSecurityPolicy, "X-Content-Type-Options": "nosniff", "Referrer-Policy": "same-origin", } { if got := rec.Header().Get(header); got != want { t.Errorf("%s = %q, want %q", header, got, want) } } }