enki b3204ea07a
Some checks are pending
CI Pipeline / Run Tests (push) Waiting to run
CI Pipeline / Lint Code (push) Waiting to run
CI Pipeline / Security Scan (push) Waiting to run
CI Pipeline / Build Docker Images (push) Blocked by required conditions
CI Pipeline / E2E Tests (push) Blocked by required conditions
first commit
2025-08-18 00:40:15 -07:00

152 lines
4.8 KiB
Go

package middleware
import (
"net/http"
"strings"
)
// SecurityHeaders adds comprehensive security headers to responses
func SecurityHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Content Security Policy
csp := strings.Join([]string{
"default-src 'self'",
"script-src 'self' 'unsafe-inline'", // Allow inline scripts for our JS
"style-src 'self' 'unsafe-inline'", // Allow inline styles for our CSS
"img-src 'self' data: https:", // Allow images from self, data URLs, and HTTPS
"media-src 'self'", // Media files from self only
"font-src 'self'", // Fonts from self only
"connect-src 'self' wss: ws:", // Allow WebSocket connections for Nostr
"object-src 'none'", // Block objects/embeds
"frame-src 'none'", // Block frames
"base-uri 'self'", // Base URI restriction
"form-action 'self'", // Form submissions to self only
}, "; ")
w.Header().Set("Content-Security-Policy", csp)
// HTTP Strict Transport Security (only if HTTPS)
if r.TLS != nil {
w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
}
// Prevent MIME type sniffing
w.Header().Set("X-Content-Type-Options", "nosniff")
// XSS Protection
w.Header().Set("X-XSS-Protection", "1; mode=block")
// Prevent clickjacking
w.Header().Set("X-Frame-Options", "DENY")
// Referrer Policy - privacy-focused
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
// Permissions Policy - restrict potentially dangerous features
permissions := strings.Join([]string{
"camera=()",
"microphone=()",
"geolocation=()",
"payment=()",
"usb=()",
"magnetometer=()",
"gyroscope=()",
"accelerometer=()",
}, ", ")
w.Header().Set("Permissions-Policy", permissions)
// Remove server information
w.Header().Set("Server", "")
// Cache control for sensitive endpoints
if strings.Contains(r.URL.Path, "/api/users/") ||
strings.Contains(r.URL.Path, "/api/auth/") ||
strings.Contains(r.URL.Path, "/admin") {
w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, private")
w.Header().Set("Pragma", "no-cache")
w.Header().Set("Expires", "0")
}
next.ServeHTTP(w, r)
})
}
// InputSanitization middleware to validate and sanitize inputs
func InputSanitization(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Validate Content-Length for uploads
if r.Method == http.MethodPost || r.Method == http.MethodPut {
if contentLength := r.Header.Get("Content-Length"); contentLength != "" {
// Prevent negative or extremely large content lengths
if strings.HasPrefix(contentLength, "-") {
http.Error(w, "Invalid Content-Length", http.StatusBadRequest)
return
}
}
}
// Validate and sanitize query parameters
query := r.URL.Query()
for key, values := range query {
for i, value := range values {
// Remove null bytes and control characters
cleaned := strings.Map(func(r rune) rune {
if r == 0 || (r < 32 && r != 9 && r != 10 && r != 13) {
return -1
}
return r
}, value)
query[key][i] = cleaned
}
}
r.URL.RawQuery = query.Encode()
// Validate User-Agent to prevent empty or suspicious values
userAgent := r.Header.Get("User-Agent")
if userAgent == "" {
r.Header.Set("User-Agent", "unknown")
} else if len(userAgent) > 500 {
// Truncate extremely long user agents
r.Header.Set("User-Agent", userAgent[:500])
}
next.ServeHTTP(w, r)
})
}
// RequestSizeLimit middleware to limit request body size
func RequestSizeLimit(maxBytes int64) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Limit request body size
r.Body = http.MaxBytesReader(w, r.Body, maxBytes)
next.ServeHTTP(w, r)
})
}
}
// AntiCrawler middleware to discourage automated scraping
func AntiCrawler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userAgent := strings.ToLower(r.Header.Get("User-Agent"))
// List of known bot/crawler user agents
crawlerPatterns := []string{
"bot", "crawler", "spider", "scraper", "archive",
"wget", "curl", "python-requests", "go-http-client",
"facebookexternalhit", "twitterbot", "linkedinbot",
}
for _, pattern := range crawlerPatterns {
if strings.Contains(userAgent, pattern) {
// For file downloads, return 403
if strings.Contains(r.URL.Path, "/download/") ||
strings.Contains(r.URL.Path, "/stream/") {
http.Error(w, "Automated access not allowed", http.StatusForbidden)
return
}
}
}
next.ServeHTTP(w, r)
})
}