package middleware import ( "net/http" "strings" "github.com/gin-gonic/gin" ) func SecurityHeaders() gin.HandlerFunc { return func(c *gin.Context) { h := c.Writer.Header() h.Set("X-Content-Type-Options", "nosniff") h.Set("X-Frame-Options", "DENY") h.Set("Referrer-Policy", "strict-origin-when-cross-origin") h.Set("X-Permitted-Cross-Domain-Policies", "none") h.Set("Cross-Origin-Opener-Policy", "same-origin") h.Set("Permissions-Policy", "camera=(), microphone=(), geolocation=(), payment=()") if isHTTPSRequest(c) { h.Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains") } if isSensitivePath(c.Request.URL.Path) { h.Set("Cache-Control", "no-store") h.Set("Pragma", "no-cache") } c.Next() } } func isHTTPSRequest(c *gin.Context) bool { if c == nil || c.Request == nil { return false } if c.Request.TLS != nil { return true } return strings.EqualFold(c.GetHeader("X-Forwarded-Proto"), "https") } func isSensitivePath(path string) bool { switch strings.TrimSpace(path) { case "/api/auth/login", "/api/auth/refresh", "/api/auth/me", "/api/auth/logout", "/api/auth/password-key", "/api/ops/auth/login", "/api/ops/auth/me", "/api/ops/auth/password", "/api/ops/auth/password-key": return true default: return false } } type CORSConfig struct { AllowedOrigins []string AllowAll bool } func CORSWithConfig(cfg CORSConfig) gin.HandlerFunc { allowedOrigins := compactOrigins(cfg.AllowedOrigins) allowedSet := make(map[string]struct{}, len(allowedOrigins)) for _, origin := range allowedOrigins { allowedSet[origin] = struct{}{} } return func(c *gin.Context) { origin := strings.TrimSpace(c.GetHeader("Origin")) allowedOrigin := "" switch { case cfg.AllowAll: allowedOrigin = "*" case origin != "": if _, ok := allowedSet[origin]; ok { allowedOrigin = origin } } if allowedOrigin != "" { c.Header("Access-Control-Allow-Origin", allowedOrigin) if allowedOrigin != "*" { c.Header("Vary", "Origin") } } c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS") c.Header("Access-Control-Allow-Headers", "Origin, Content-Type, Authorization, X-Request-ID, X-Internal-Metrics-Token") c.Header("Access-Control-Expose-Headers", "X-Request-ID") c.Header("Access-Control-Max-Age", "86400") if c.Request.Method == http.MethodOptions { if origin != "" && allowedOrigin == "" { c.AbortWithStatus(http.StatusForbidden) return } c.AbortWithStatus(http.StatusNoContent) return } c.Next() } } func compactOrigins(values []string) []string { out := make([]string, 0, len(values)) seen := make(map[string]struct{}, len(values)) for _, value := range values { value = strings.TrimSpace(value) if value == "" { continue } if _, ok := seen[value]; ok { continue } seen[value] = struct{}{} out = append(out, value) } return out }