package middleware import ( "os" "strconv" "strings" "github.com/gin-gonic/gin" ) // SecurityHeadersConfig holds configuration for security headers. type SecurityHeadersConfig struct { // X-Content-Type-Options ContentTypeOptions string // X-Frame-Options FrameOptions string // X-XSS-Protection (legacy but useful for older browsers) XSSProtection string // Strict-Transport-Security HSTSEnabled bool HSTSMaxAge int HSTSIncludeSubdomains bool HSTSPreload bool // Content-Security-Policy CSPEnabled bool CSPPolicy string // Referrer-Policy ReferrerPolicy string // Permissions-Policy PermissionsPolicy string // Cross-Origin headers CrossOriginOpenerPolicy string CrossOriginResourcePolicy string // Development mode (relaxes some restrictions) DevelopmentMode bool // Excluded paths (e.g., health checks) ExcludedPaths []string } // DefaultSecurityHeadersConfig returns sensible default configuration. func DefaultSecurityHeadersConfig() SecurityHeadersConfig { env := os.Getenv("ENVIRONMENT") isDev := env == "" || strings.ToLower(env) == "development" || strings.ToLower(env) == "dev" return SecurityHeadersConfig{ ContentTypeOptions: "nosniff", FrameOptions: "DENY", XSSProtection: "1; mode=block", HSTSEnabled: true, HSTSMaxAge: 31536000, // 1 year HSTSIncludeSubdomains: true, HSTSPreload: false, CSPEnabled: true, CSPPolicy: getDefaultCSP(isDev), ReferrerPolicy: "strict-origin-when-cross-origin", PermissionsPolicy: "geolocation=(), microphone=(), camera=()", CrossOriginOpenerPolicy: "same-origin", CrossOriginResourcePolicy: "same-origin", DevelopmentMode: isDev, ExcludedPaths: []string{"/health", "/metrics", "/api/v1/health"}, } } // getDefaultCSP returns a sensible default CSP for the environment. func getDefaultCSP(isDevelopment bool) string { if isDevelopment { return "default-src 'self' localhost:* ws://localhost:*; " + "script-src 'self' 'unsafe-inline' 'unsafe-eval'; " + "style-src 'self' 'unsafe-inline'; " + "img-src 'self' data: https: blob:; " + "font-src 'self' data:; " + "connect-src 'self' localhost:* ws://localhost:* https:; " + "frame-ancestors 'self'" } return "default-src 'self'; " + "script-src 'self' 'unsafe-inline'; " + "style-src 'self' 'unsafe-inline'; " + "img-src 'self' data: https:; " + "font-src 'self' data:; " + "connect-src 'self' https://breakpilot.app https://*.breakpilot.app; " + "frame-ancestors 'none'" } // buildHSTSHeader builds the Strict-Transport-Security header value. func (c *SecurityHeadersConfig) buildHSTSHeader() string { parts := []string{"max-age=" + strconv.Itoa(c.HSTSMaxAge)} if c.HSTSIncludeSubdomains { parts = append(parts, "includeSubDomains") } if c.HSTSPreload { parts = append(parts, "preload") } return strings.Join(parts, "; ") } // isExcludedPath checks if the path should be excluded from security headers. func (c *SecurityHeadersConfig) isExcludedPath(path string) bool { for _, excluded := range c.ExcludedPaths { if path == excluded { return true } } return false } // SecurityHeaders returns a middleware that adds security headers to all responses. // // Usage: // // r.Use(middleware.SecurityHeaders()) // // // Or with custom config: // config := middleware.DefaultSecurityHeadersConfig() // config.CSPPolicy = "default-src 'self'" // r.Use(middleware.SecurityHeadersWithConfig(config)) func SecurityHeaders() gin.HandlerFunc { return SecurityHeadersWithConfig(DefaultSecurityHeadersConfig()) } // SecurityHeadersWithConfig returns a security headers middleware with custom configuration. func SecurityHeadersWithConfig(config SecurityHeadersConfig) gin.HandlerFunc { return func(c *gin.Context) { // Skip for excluded paths if config.isExcludedPath(c.Request.URL.Path) { c.Next() return } // Always add these headers c.Header("X-Content-Type-Options", config.ContentTypeOptions) c.Header("X-Frame-Options", config.FrameOptions) c.Header("X-XSS-Protection", config.XSSProtection) c.Header("Referrer-Policy", config.ReferrerPolicy) // HSTS (only in production or if explicitly enabled) if config.HSTSEnabled && !config.DevelopmentMode { c.Header("Strict-Transport-Security", config.buildHSTSHeader()) } // Content-Security-Policy if config.CSPEnabled && config.CSPPolicy != "" { c.Header("Content-Security-Policy", config.CSPPolicy) } // Permissions-Policy if config.PermissionsPolicy != "" { c.Header("Permissions-Policy", config.PermissionsPolicy) } // Cross-Origin headers (only in production) if !config.DevelopmentMode { c.Header("Cross-Origin-Opener-Policy", config.CrossOriginOpenerPolicy) c.Header("Cross-Origin-Resource-Policy", config.CrossOriginResourcePolicy) } c.Next() } }