veza/veza-backend-api/internal/middleware/cors.go
senke 872e42d81c refactor(backend): replace 40 fmt.Printf calls with zap structured logging
CLN-03: router.go, track/service.go, upload_validator.go, cors.go,
playlist_handler.go, and mfa.go now use zap.L() or local logger
for structured logging instead of fmt.Printf.
2026-02-22 17:44:38 +01:00

149 lines
5.2 KiB
Go

package middleware
import (
"fmt"
"os"
"strings"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
// ValidateCORSConfiguration valide la configuration CORS pour éviter les problèmes de sécurité
// INT-018: Amélioration de la validation CORS pour la production
// - En production: retourne une erreur si wildcard est utilisé ou si CORS est vide
// - En développement: log un warning mais ne bloque pas
func ValidateCORSConfiguration(allowedOrigins []string, environment string, logger *zap.Logger) error {
hasWildcard := false
weakOrigins := []string{}
for _, origin := range allowedOrigins {
// Détecter les wildcards
if origin == "*" || strings.Contains(origin, "*") {
hasWildcard = true
weakOrigins = append(weakOrigins, origin)
}
}
// INT-018: Validation stricte en production
if environment == "production" {
// 1. CORS ne doit pas être vide en production
if len(allowedOrigins) == 0 {
errMsg := "CORS_ALLOWED_ORIGINS is required in production environment. Empty CORS origins will reject all CORS requests, making the service inaccessible from frontend"
if logger != nil {
logger.Error(errMsg,
zap.String("environment", environment),
zap.String("recommendation", "Set CORS_ALLOWED_ORIGINS with explicit origins (e.g., CORS_ALLOWED_ORIGINS=https://app.veza.com,https://www.veza.com)"),
)
}
return fmt.Errorf("%s. Please set CORS_ALLOWED_ORIGINS with explicit origins", errMsg)
}
// 2. CORS ne doit pas contenir de wildcard en production
// CORS spec: credentials=true ne peut pas être utilisé avec Access-Control-Allow-Origin: *
// C'est une faille de sécurité car cela permettrait à n'importe quel site d'accéder aux credentials
if hasWildcard {
errMsg := fmt.Sprintf(
"CORS wildcard origins (%v) are not allowed in production environment with credentials=true. "+
"This violates CORS specification and is a security risk. "+
"Use specific origins instead of wildcards.",
weakOrigins,
)
if logger != nil {
logger.Error(errMsg,
zap.Strings("weak_origins", weakOrigins),
zap.String("environment", environment),
zap.String("recommendation", "Use specific origins instead of wildcards (e.g., CORS_ALLOWED_ORIGINS=https://app.veza.com,https://www.veza.com)"),
)
}
return fmt.Errorf("%s", errMsg)
}
} else {
// INT-018: En développement/staging, log un warning mais ne bloque pas
if hasWildcard {
warningMsg := fmt.Sprintf(
"SECURITY WARNING: CORS configuration allows wildcard origins (%v) with credentials=true. "+
"This is insecure and violates CORS specification. "+
"Use specific origins instead of wildcards when credentials=true. "+
"This will cause startup failure in production.",
weakOrigins,
)
if logger != nil {
logger.Warn(warningMsg,
zap.Strings("weak_origins", weakOrigins),
zap.String("environment", environment),
zap.String("recommendation", "Use specific origins instead of wildcards for production"),
)
} else {
// Fallback si logger n'est pas disponible
zap.L().Warn(warningMsg,
zap.Strings("weak_origins", weakOrigins),
zap.String("environment", environment),
zap.String("recommendation", "Use specific origins instead of wildcards for production"),
)
}
}
}
return nil
}
// CORS middleware pour gérer les en-têtes CORS avec whitelist d'origins configurable
// allowedOrigins: liste des origines autorisées (ex: []string{"http://localhost:3000", "https://example.com"})
// Si "*" est dans la liste, toutes les origines sont autorisées
// ATTENTION: L'utilisation de "*" avec credentials=true est interdite par la spec CORS
func CORS(allowedOrigins []string) gin.HandlerFunc {
return func(c *gin.Context) {
origin := c.GetHeader("Origin")
// Vérifier si l'origine est autorisée
if isAllowedOrigin(origin, allowedOrigins) {
c.Header("Access-Control-Allow-Origin", origin)
}
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
c.Header("Access-Control-Allow-Headers", "Authorization, Content-Type, X-Requested-With, X-CSRF-Token, X-API-Version, x-api-version")
c.Header("Access-Control-Allow-Credentials", "true")
c.Header("Access-Control-Expose-Headers", "X-CSRF-Token, X-Request-ID, Content-Range")
c.Header("Access-Control-Max-Age", "86400") // Cache preflight pour 24h
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
}
// isAllowedOrigin vérifie si une origine est dans la liste des origines autorisées
func isAllowedOrigin(origin string, allowed []string) bool {
// Sécurité par défaut : si liste vide, on rejette tout
if len(allowed) == 0 {
return false
}
for _, o := range allowed {
// Permettre toutes les origines si "*" est dans la liste
// ATTENTION: À utiliser seulement en dev
if o == "*" {
return true
}
if o == origin {
return true
}
}
return false
}
// CORSDefault crée un middleware CORS avec une whitelist par défaut.
// SEC-016: DEPRECATED — DO NOT USE IN PRODUCTION. Panics if APP_ENV=production.
func CORSDefault() gin.HandlerFunc {
if os.Getenv("APP_ENV") == "production" {
panic("CORSDefault() must not be used in production - use CORS() with explicit origins")
}
return CORS([]string{"*"})
}