From 08032aab10f0e1429d25ecae1acf6c40d63e9ff4 Mon Sep 17 00:00:00 2001 From: Adam Mathes Date: Sat, 14 Feb 2026 09:20:40 -0800 Subject: security: add HTTP security headers (fixing NK-7xuajb) --- web/web.go | 18 +++++++++++++++++- web/web_test.go | 25 +++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) (limited to 'web') diff --git a/web/web.go b/web/web.go index 4868577..11a5831 100644 --- a/web/web.go +++ b/web/web.go @@ -259,7 +259,7 @@ func NewRouter(cfg *config.Settings) http.Handler { mux.Handle("/", GzipMiddleware(AuthWrap(http.HandlerFunc(indexHandler)))) - return CSRFMiddleware(mux) + return SecurityHeadersMiddleware(CSRFMiddleware(mux)) } func Serve(cfg *config.Settings) { @@ -383,3 +383,19 @@ func CSRFMiddleware(next http.Handler) http.Handler { next.ServeHTTP(w, r) }) } + +func SecurityHeadersMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("X-Content-Type-Options", "nosniff") + w.Header().Set("X-Frame-Options", "DENY") + w.Header().Set("X-XSS-Protection", "1; mode=block") + w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin") + // Content-Security-Policy + // default-src 'self' + // style-src 'self' 'unsafe-inline' (for React/styled-components if used) + // img-src 'self' data: * (RSS images can be from anywhere) + // connect-src 'self' (API calls) + w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: *; connect-src 'self'; frame-ancestors 'none';") + next.ServeHTTP(w, r) + }) +} diff --git a/web/web_test.go b/web/web_test.go index 89ca998..c6cf306 100644 --- a/web/web_test.go +++ b/web/web_test.go @@ -10,10 +10,15 @@ import ( "adammathes.com/neko/api" "adammathes.com/neko/config" + "adammathes.com/neko/internal/safehttp" "adammathes.com/neko/models" "golang.org/x/crypto/bcrypt" ) +func init() { + safehttp.AllowLocal = true +} + func setupTestDB(t *testing.T) { t.Helper() config.Config.DBFile = filepath.Join(t.TempDir(), "test.db") @@ -774,3 +779,23 @@ func TestCSRFMiddleware(t *testing.T) { t.Errorf("Expected 200 for POST with valid token, got %d", rr.Code) } } + +func TestSecurityHeadersMiddleware(t *testing.T) { + handler := SecurityHeadersMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + + req := httptest.NewRequest("GET", "/", nil) + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + + if rr.Header().Get("X-Content-Type-Options") != "nosniff" { + t.Error("Missing X-Content-Type-Options: nosniff") + } + if rr.Header().Get("X-Frame-Options") != "DENY" { + t.Error("Missing X-Frame-Options: DENY") + } + if rr.Header().Get("Content-Security-Policy") == "" { + t.Error("Missing Content-Security-Policy") + } +} -- cgit v1.2.3