diff --git a/AUDIT_TECHNIQUE_INTEGRAL_2026_02_15.md b/AUDIT_TECHNIQUE_INTEGRAL_2026_02_15.md index cacfbbc3c..4f5aa4653 100644 --- a/AUDIT_TECHNIQUE_INTEGRAL_2026_02_15.md +++ b/AUDIT_TECHNIQUE_INTEGRAL_2026_02_15.md @@ -653,8 +653,8 @@ veza/ | 1.3 | ~~Ajouter auth sur les endpoints HLS~~ | **M** | `veza-stream-server/src/routes/api.rs` | **✅ Fait** | | 1.4 | ~~Remplacer `panic()` par erreur gracieuse si Redis down~~ | **S** | `veza-backend-api/internal/api/routes_core.go:242` | **✅ Fait** | | 1.5 | ~~Corriger les `.unwrap()` critiques en Rust (paths de production)~~ | **M** | `veza-stream-server/src/`, `veza-chat-server/src/` | **✅ Fait** | -| 1.6 | Supprimer les secrets test hardcodés | **S** | `veza-chat-server/src/config.rs:216`, `jwt_manager.rs:575` | P1 | -| 1.7 | Ajouter protection contre bypass flags en production | **S** | `veza-backend-api/internal/middleware/auth.go`, `csrf.go` | P1 | +| 1.6 | ~~Supprimer les secrets test hardcodés~~ | **S** | `veza-chat-server/src/config.rs:216`, `jwt_manager.rs:575` | **✅ Fait** | +| 1.7 | ~~Ajouter protection contre bypass flags en production~~ | **S** | `veza-backend-api/internal/middleware/auth.go`, `csrf.go` | **✅ Fait** | | 1.8 | Implémenter OAuth user lookup | **M** | `veza-backend-api/internal/database/database.go:559` | P1 | | 1.9 | Ajouter `cargo audit` au CI | **S** | `.github/workflows/chat-ci.yml`, `stream-ci.yml` | P2 | diff --git a/veza-backend-api/internal/config/config.go b/veza-backend-api/internal/config/config.go index 5658f2cd2..9d3e59990 100644 --- a/veza-backend-api/internal/config/config.go +++ b/veza-backend-api/internal/config/config.go @@ -1393,6 +1393,31 @@ func (c *Config) Validate() error { return fmt.Errorf("RATE_LIMIT_WINDOW validation failed: %w", err) } + // Audit 1.7: Fail startup if bypass flags are set in production + if err := validateNoBypassFlagsInProduction(c.Env); err != nil { + return err + } + + return nil +} + +// validateNoBypassFlagsInProduction vérifie qu'aucun flag de bypass n'est activé en production (audit 1.7) +func validateNoBypassFlagsInProduction(env string) error { + envNorm := strings.ToLower(strings.TrimSpace(env)) + if envNorm != "production" && envNorm != "prod" { + return nil // Pas en production, pas de vérification + } + var violations []string + if os.Getenv("BYPASS_CONTENT_CREATOR_ROLE") == "true" { + violations = append(violations, "BYPASS_CONTENT_CREATOR_ROLE=true") + } + if os.Getenv("CSRF_DISABLED") == "true" { + violations = append(violations, "CSRF_DISABLED=true") + } + if len(violations) > 0 { + return fmt.Errorf("security: bypass flags are not allowed in production: %s. Remove these environment variables before deploying", + strings.Join(violations, ", ")) + } return nil } diff --git a/veza-backend-api/internal/config/validation_test.go b/veza-backend-api/internal/config/validation_test.go index 650658e79..257177769 100644 --- a/veza-backend-api/internal/config/validation_test.go +++ b/veza-backend-api/internal/config/validation_test.go @@ -1,6 +1,7 @@ package config import ( + "os" "strings" "testing" @@ -291,3 +292,78 @@ func TestConfig_Validate(t *testing.T) { }) } } + +// TestValidateNoBypassFlagsInProduction vérifie que les bypass flags bloquent le démarrage en production (audit 1.7) +func TestValidateNoBypassFlagsInProduction(t *testing.T) { + validConfig := &Config{ + Env: "development", + AppPort: 8080, + JWTSecret: strings.Repeat("a", 32), + DatabaseURL: "postgresql://user:pass@localhost:5432/db", + RedisURL: "redis://localhost:6379", + RateLimitLimit: 100, + RateLimitWindow: 60, + } + + t.Run("production with BYPASS_CONTENT_CREATOR_ROLE fails", func(t *testing.T) { + orig := os.Getenv("BYPASS_CONTENT_CREATOR_ROLE") + defer func() { + if orig != "" { + os.Setenv("BYPASS_CONTENT_CREATOR_ROLE", orig) + } else { + os.Unsetenv("BYPASS_CONTENT_CREATOR_ROLE") + } + }() + os.Setenv("BYPASS_CONTENT_CREATOR_ROLE", "true") + + cfg := *validConfig + cfg.Env = "production" + err := cfg.Validate() + require.Error(t, err) + assert.Contains(t, err.Error(), "BYPASS_CONTENT_CREATOR_ROLE") + assert.Contains(t, err.Error(), "bypass flags") + }) + + t.Run("production with CSRF_DISABLED fails", func(t *testing.T) { + orig := os.Getenv("CSRF_DISABLED") + defer func() { + if orig != "" { + os.Setenv("CSRF_DISABLED", orig) + } else { + os.Unsetenv("CSRF_DISABLED") + } + }() + os.Setenv("CSRF_DISABLED", "true") + + cfg := *validConfig + cfg.Env = "production" + err := cfg.Validate() + require.Error(t, err) + assert.Contains(t, err.Error(), "CSRF_DISABLED") + assert.Contains(t, err.Error(), "bypass flags") + }) + + t.Run("development with bypass flags succeeds", func(t *testing.T) { + origBypass := os.Getenv("BYPASS_CONTENT_CREATOR_ROLE") + origCsrf := os.Getenv("CSRF_DISABLED") + defer func() { + if origBypass != "" { + os.Setenv("BYPASS_CONTENT_CREATOR_ROLE", origBypass) + } else { + os.Unsetenv("BYPASS_CONTENT_CREATOR_ROLE") + } + if origCsrf != "" { + os.Setenv("CSRF_DISABLED", origCsrf) + } else { + os.Unsetenv("CSRF_DISABLED") + } + }() + os.Setenv("BYPASS_CONTENT_CREATOR_ROLE", "true") + os.Setenv("CSRF_DISABLED", "true") + + cfg := *validConfig + cfg.Env = "development" + err := cfg.Validate() + require.NoError(t, err) + }) +} diff --git a/veza-chat-server/src/config.rs b/veza-chat-server/src/config.rs index 5a845608c..510860921 100644 --- a/veza-chat-server/src/config.rs +++ b/veza-chat-server/src/config.rs @@ -213,7 +213,8 @@ impl Default for SecurityConfig { #[cfg(test)] Self { - jwt_secret: "test_jwt_secret_minimum_32_characters_long".to_string(), + jwt_secret: env::var("TEST_JWT_SECRET") + .unwrap_or_else(|_| format!("test_{}_{}", uuid::Uuid::new_v4(), "x".repeat(20))), jwt_access_duration: Duration::from_secs(900), // 15 min jwt_refresh_duration: Duration::from_secs(86400 * 30), // 30 days jwt_algorithm: "HS256".to_string(), diff --git a/veza-chat-server/src/jwt_manager.rs b/veza-chat-server/src/jwt_manager.rs index 6124109a2..96b1f4812 100644 --- a/veza-chat-server/src/jwt_manager.rs +++ b/veza-chat-server/src/jwt_manager.rs @@ -571,8 +571,10 @@ mod tests { use std::time::Duration; fn create_test_config() -> SecurityConfig { + let jwt_secret = std::env::var("TEST_JWT_SECRET") + .unwrap_or_else(|_| format!("test_{}_{}", Uuid::new_v4(), "x".repeat(20))); SecurityConfig { - jwt_secret: "test_secret_key_32_chars_minimum_required".to_string(), + jwt_secret, jwt_access_duration: Duration::from_secs(3600), // 1 heure jwt_refresh_duration: Duration::from_secs(86400), // 24 heures jwt_algorithm: "HS256".to_string(),