From 369eef7bc3b6885de7d7bf8ef0c7a30f65aa29a7 Mon Sep 17 00:00:00 2001 From: clawbot Date: Tue, 10 Mar 2026 03:15:02 -0700 Subject: [PATCH 1/2] feat: add Content-Security-Policy middleware MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add CSP header to all HTTP responses for defense-in-depth against XSS. The policy restricts all resource loading to same-origin and disables dangerous features (object embeds, framing, base tag injection). The embedded SPA requires no inline scripts or inline style attributes (Preact applies styles programmatically via DOM properties), so a strict policy without 'unsafe-inline' works correctly. Directives: default-src 'self' — baseline same-origin restriction script-src 'self' — same-origin scripts only style-src 'self' — same-origin stylesheets only connect-src 'self' — same-origin fetch/XHR only img-src 'self' — same-origin images only font-src 'self' — same-origin fonts only object-src 'none' — no plugin content frame-ancestors 'none' — prevent clickjacking base-uri 'self' — prevent base tag injection form-action 'self' — restrict form submissions --- internal/middleware/middleware.go | 33 +++++++++++++++++++++++++++++++ internal/server/routes.go | 1 + 2 files changed, 34 insertions(+) diff --git a/internal/middleware/middleware.go b/internal/middleware/middleware.go index e8260ca..e2af1d2 100644 --- a/internal/middleware/middleware.go +++ b/internal/middleware/middleware.go @@ -180,3 +180,36 @@ func (mware *Middleware) MetricsAuth() func(http.Handler) http.Handler { }, ) } + +// cspPolicy is the Content-Security-Policy header value applied to all +// responses. The embedded SPA loads scripts and styles from same-origin +// files only (no inline scripts or inline style attributes), so a strict +// policy works without 'unsafe-inline'. +const cspPolicy = "default-src 'self'; " + + "script-src 'self'; " + + "style-src 'self'; " + + "connect-src 'self'; " + + "img-src 'self'; " + + "font-src 'self'; " + + "object-src 'none'; " + + "frame-ancestors 'none'; " + + "base-uri 'self'; " + + "form-action 'self'" + +// CSP returns middleware that sets the Content-Security-Policy header on +// every response for defense-in-depth against XSS. +func (mware *Middleware) CSP() func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc( + func( + writer http.ResponseWriter, + request *http.Request, + ) { + writer.Header().Set( + "Content-Security-Policy", + cspPolicy, + ) + next.ServeHTTP(writer, request) + }) + } +} diff --git a/internal/server/routes.go b/internal/server/routes.go index 9cc0103..ced61e4 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -29,6 +29,7 @@ func (srv *Server) SetupRoutes() { } srv.router.Use(srv.mw.CORS()) + srv.router.Use(srv.mw.CSP()) srv.router.Use(middleware.Timeout(routeTimeout)) if srv.sentryEnabled { -- 2.49.1 From d6cfb2e8972e81fb82f1672b69a6acaec913b701 Mon Sep 17 00:00:00 2001 From: clawbot Date: Tue, 10 Mar 2026 03:15:49 -0700 Subject: [PATCH 2/2] docs: document CSP header in Security Model section --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 146dad6..2d061a3 100644 --- a/README.md +++ b/README.md @@ -1624,6 +1624,10 @@ authenticity. termination. - **CORS**: The server allows all origins by default (`Access-Control-Allow-Origin: *`). Restrict this in production via reverse proxy configuration if needed. +- **Content-Security-Policy**: The server sets a strict CSP header on all + responses, restricting resource loading to same-origin and disabling + dangerous features (object embeds, framing, base tag injection). The + embedded SPA works without `'unsafe-inline'` for scripts or styles. --- -- 2.49.1