veza/veza-backend-api/internal/middleware/cors.go
senke ad60247f33 feat: global update including storybook setup and backend fixes
- Web: Setup Storybook, added addons, configured Tailwind, added stories for UI components.
- Backend: Updated API router, database, workers, and auth in common.
- Stream Server: Removed SQLx queries and updated auth.
- Docs & Scripts: Updated documentation and recovery scripts.
2026-02-02 19:34:14 +01:00

141 lines
4.9 KiB
Go

package middleware
import (
"fmt"
"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
fmt.Printf("⚠️ %s\n", warningMsg)
}
}
}
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
// Utile pour compatibilité avec le code existant
func CORSDefault() gin.HandlerFunc {
return CORS([]string{"*"})
}