From e20c507dccd57b53ef7f22ef0221fde46680cfea Mon Sep 17 00:00:00 2001 From: LinYushen Date: Mon, 13 Apr 2026 12:53:39 +0800 Subject: [PATCH] fix(security): add Content-Security-Policy response header (#822) Adds CSP middleware to the global middleware chain as a browser-level defense against XSS: script-src 'self', object-src 'none', frame-ancestors 'none', base-uri 'self', form-action 'self'. Co-authored-by: Claude Opus 4.6 (1M context) --- server/cmd/server/router.go | 1 + server/internal/middleware/csp.go | 20 ++++++++++++++ server/internal/middleware/csp_test.go | 36 ++++++++++++++++++++++++++ 3 files changed, 57 insertions(+) create mode 100644 server/internal/middleware/csp.go create mode 100644 server/internal/middleware/csp_test.go diff --git a/server/cmd/server/router.go b/server/cmd/server/router.go index 8bb956606..e9bc9ecd1 100644 --- a/server/cmd/server/router.go +++ b/server/cmd/server/router.go @@ -78,6 +78,7 @@ func NewRouter(pool *pgxpool.Pool, hub *realtime.Hub, bus *events.Bus) chi.Route r.Use(chimw.RequestID) r.Use(middleware.RequestLogger) r.Use(chimw.Recoverer) + r.Use(middleware.ContentSecurityPolicy) origins := allowedOrigins() // Share allowed origins with WebSocket origin checker. diff --git a/server/internal/middleware/csp.go b/server/internal/middleware/csp.go new file mode 100644 index 000000000..4fefa2a5e --- /dev/null +++ b/server/internal/middleware/csp.go @@ -0,0 +1,20 @@ +package middleware + +import "net/http" + +const cspHeader = "default-src 'self'; " + + "script-src 'self'; " + + "style-src 'self' 'unsafe-inline'; " + + "img-src 'self' https: data:; " + + "connect-src 'self' wss:; " + + "frame-ancestors 'none'; " + + "object-src 'none'; " + + "base-uri 'self'; " + + "form-action 'self'" + +func ContentSecurityPolicy(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Security-Policy", cspHeader) + next.ServeHTTP(w, r) + }) +} diff --git a/server/internal/middleware/csp_test.go b/server/internal/middleware/csp_test.go new file mode 100644 index 000000000..35ab93eba --- /dev/null +++ b/server/internal/middleware/csp_test.go @@ -0,0 +1,36 @@ +package middleware + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestContentSecurityPolicy(t *testing.T) { + handler := ContentSecurityPolicy(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + + req := httptest.NewRequest(http.MethodGet, "/", nil) + rec := httptest.NewRecorder() + handler.ServeHTTP(rec, req) + + csp := rec.Header().Get("Content-Security-Policy") + if csp == "" { + t.Fatal("Content-Security-Policy header is missing") + } + + required := []string{ + "script-src 'self'", + "object-src 'none'", + "frame-ancestors 'none'", + "base-uri 'self'", + "form-action 'self'", + } + for _, directive := range required { + if !strings.Contains(csp, directive) { + t.Errorf("CSP missing directive %q; got: %s", directive, csp) + } + } +}