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.
149 lines
5.2 KiB
Go
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{"*"})
|
|
}
|