Merge pull request #428 from Molecule-AI/fix/securityheaders-test-stale-csp

fix(tests): CSP test fragment-match instead of exact-match
This commit is contained in:
Hongming Wang 2026-04-16 03:07:05 -07:00 committed by GitHub
commit 3cf5fd117a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -3,6 +3,7 @@ package middleware
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/gin-gonic/gin"
@ -33,7 +34,6 @@ func TestSecurityHeaders(t *testing.T) {
}{
{"X-Content-Type-Options", "nosniff"},
{"X-Frame-Options", "DENY"},
{"Content-Security-Policy", "default-src 'self'"},
{"Strict-Transport-Security", "max-age=31536000; includeSubDomains"},
// #282: regression guards for the two headers that were
// documented in CLAUDE.md but missing from the implementation.
@ -47,6 +47,25 @@ func TestSecurityHeaders(t *testing.T) {
t.Errorf("header %s = %q, want %q", tt.header, got, tt.want)
}
}
// CSP: widened to allow Next.js inline scripts/styles + data:/blob:
// images because the canvas is reverse-proxied through the same
// gin middleware stack. Assert the policy starts with the tight
// default-src and contains each expected directive — exact-match
// would brittle-break every time we tune a subsource list.
csp := w.Header().Get("Content-Security-Policy")
for _, fragment := range []string{
"default-src 'self'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: blob:",
"connect-src 'self' ws: wss:",
"font-src 'self' data:",
} {
if !strings.Contains(csp, fragment) {
t.Errorf("CSP missing expected fragment %q (full CSP: %q)", fragment, csp)
}
}
}
func TestSecurityHeadersPresenceOnMultipleRoutes(t *testing.T) {
@ -75,8 +94,13 @@ func TestSecurityHeadersPresenceOnMultipleRoutes(t *testing.T) {
if v := w2.Header().Get("Strict-Transport-Security"); v != "max-age=31536000; includeSubDomains" {
t.Errorf("POST /b: Strict-Transport-Security = %q, want max-age=31536000; includeSubDomains", v)
}
if v := w2.Header().Get("Content-Security-Policy"); v != "default-src 'self'" {
t.Errorf("POST /b: Content-Security-Policy = %q, want default-src 'self'", v)
// Fragment-match rather than exact — CSP subsource lists get tuned
// without changing the security posture. Test intent is "CSP is
// present + starts with the tight default-src", not "CSP matches
// this exact byte-for-byte string".
csp := w2.Header().Get("Content-Security-Policy")
if !strings.Contains(csp, "default-src 'self'") {
t.Errorf("POST /b: CSP missing default-src 'self' (full: %q)", csp)
}
}