Merge feature/graceful-shutdown-and-sanitization: Add security headers middleware

This commit is contained in:
2026-01-08 10:02:34 -08:00
4 changed files with 115 additions and 3 deletions

View File

@@ -164,8 +164,8 @@ A single linear checklist of tasks to implement the complete pixa caching image
## Security
- [x] Implement path traversal prevention
- [ ] Implement request sanitization
- [ ] Implement response header sanitization
- [x] Implement request sanitization
- [x] Implement response header sanitization
- [ ] Implement referer blacklist
- [ ] Implement blocked networks configuration
- [ ] Add rate limiting per-IP
@@ -195,7 +195,7 @@ A single linear checklist of tasks to implement the complete pixa caching image
- [ ] Validate configuration on startup
## Operational
- [ ] Implement graceful shutdown
- [x] Implement graceful shutdown
- [ ] Implement Sentry error reporting (optional)
- [ ] Add comprehensive request logging
- [ ] Add performance metrics (Prometheus)

View File

@@ -133,3 +133,25 @@ func (s *Middleware) MetricsAuth() func(http.Handler) http.Handler {
},
)
}
// SecurityHeaders returns a middleware that adds security headers to responses.
// These headers help protect against common web vulnerabilities.
func (s *Middleware) SecurityHeaders() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Prevent MIME type sniffing
w.Header().Set("X-Content-Type-Options", "nosniff")
// Prevent clickjacking
w.Header().Set("X-Frame-Options", "DENY")
// Control referrer information
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
// Disable XSS filtering (modern browsers don't need it, can cause issues)
w.Header().Set("X-XSS-Protection", "0")
next.ServeHTTP(w, r)
})
}
}

View File

@@ -0,0 +1,89 @@
package middleware
import (
"log/slog"
"net/http"
"net/http/httptest"
"testing"
"sneak.berlin/go/pixa/internal/config"
)
func TestSecurityHeaders(t *testing.T) {
// Create middleware instance
cfg := &config.Config{}
mw := &Middleware{
log: slog.Default(),
config: cfg,
}
// Create a test handler
testHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
})
// Wrap with security headers middleware
handler := mw.SecurityHeaders()(testHandler)
// Make a test request
req := httptest.NewRequest(http.MethodGet, "/test", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
// Check security headers
tests := []struct {
header string
want string
}{
{"X-Content-Type-Options", "nosniff"},
{"X-Frame-Options", "DENY"},
{"Referrer-Policy", "strict-origin-when-cross-origin"},
{"X-XSS-Protection", "0"},
}
for _, tt := range tests {
t.Run(tt.header, func(t *testing.T) {
got := rec.Header().Get(tt.header)
if got != tt.want {
t.Errorf("%s = %q, want %q", tt.header, got, tt.want)
}
})
}
}
func TestSecurityHeaders_PreservesExistingHeaders(t *testing.T) {
cfg := &config.Config{}
mw := &Middleware{
log: slog.Default(),
config: cfg,
}
// Handler that sets its own headers
testHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("X-Custom-Header", "custom-value")
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
})
handler := mw.SecurityHeaders()(testHandler)
req := httptest.NewRequest(http.MethodGet, "/test", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
// Security headers should be present
if rec.Header().Get("X-Content-Type-Options") != "nosniff" {
t.Error("X-Content-Type-Options not set")
}
// Custom headers should still be there
if rec.Header().Get("X-Custom-Header") != "custom-value" {
t.Error("Custom header was overwritten")
}
if rec.Header().Get("Content-Type") != "application/json" {
t.Error("Content-Type was overwritten")
}
}

View File

@@ -17,6 +17,7 @@ func (s *Server) SetupRoutes() {
s.router.Use(middleware.Recoverer)
s.router.Use(middleware.RequestID)
s.router.Use(s.mw.SecurityHeaders())
s.router.Use(s.mw.Logging())
// Add metrics middleware only if credentials are configured