veza/docs/archive/backend-sessions-2026/SECURITY_FIX_JWT_REPORT.md
senke 0e7097ed1b chore(cleanup): J1 — purge 220MB debris, archive session docs (complete)
First-attempt commit 3a5c6e184 only captured the .gitignore change; the
pre-commit hook silently dropped the 343 staged moves/deletes during
lint-staged's "no matching task" path. This commit re-applies the intended
J1 content on top of bec75f143 (which was pushed in parallel).

Uses --no-verify because:
- J1 only touches .md/.json/.log/.png/binaries — zero code that would
  benefit from lint-staged, typecheck, or vitest
- The hook demonstrated it corrupts pure-rename commits in this repo
- Explicitly authorized by user for this one commit

Changes (343 total: 169 deletions + 174 renames):

Binaries purged (~167 MB):
- veza-backend-api/{server,modern-server,encrypt_oauth_tokens,seed,seed-v2}

Generated reports purged:
- 9 apps/web/lint_report*.json (~32 MB)
- 8 apps/web/tsc_*.{log,txt} + ts_*.log (TS error snapshots)
- 3 apps/web/storybook_*.json (1375+ stored errors)
- apps/web/{build_errors*,build_output,final_errors}.txt
- 70 veza-backend-api/coverage*.out + coverage_groups/ (~4 MB)
- 3 veza-backend-api/internal/handlers/*.bak

Root cleanup:
- 54 audit-*.png (visual regression baselines, ~11 MB)
- 9 stale MVP-era scripts (Jan 27, hardcoded v0.101):
  start_{iteration,mvp,recovery}.sh,
  test_{mvp_endpoints,protected_endpoints,user_journey}.sh,
  validate_v0101.sh, verify_logs_setup.sh, gen_hash.py

Session docs archived (not deleted — preserved under docs/archive/):
- 78 apps/web/*.md     → docs/archive/frontend-sessions-2026/
- 43 veza-backend-api/*.md → docs/archive/backend-sessions-2026/
- 53 docs/{RETROSPECTIVE_V,SMOKE_TEST_V,PLAN_V0_,V0_*_RELEASE_SCOPE,
          AUDIT_,PLAN_ACTION_AUDIT,REMEDIATION_PROGRESS}*.md
                        → docs/archive/v0-history/

README.md and CONTRIBUTING.md preserved in apps/web/ and veza-backend-api/.

Note: The .gitignore rules preventing recurrence were already pushed in
3a5c6e184 and remain in place — this commit does not modify .gitignore.

Refs: AUDIT_REPORT.md §11
2026-04-14 17:12:03 +02:00

14 KiB

Fix Sécurité JWT — Rapport complet

Date: 2025-01-27
Faille corrigée: JWT_SECRET avec valeur par défaut hardcodée
Sévérité: 🔴 CRITIQUE
Statut: CORRIGÉ


1. Fichiers impactés

Fichiers modifiés

  • internal/config/config.go (lignes 115-122)

    • Avant: jwtSecret := getEnv("JWT_SECRET", "your-super-secret-jwt-key")
    • Après: jwtSecret := getEnvRequired("JWT_SECRET")
    • Avant: DatabaseURL: getEnv("DATABASE_URL", "postgresql://veza:password@localhost:5432/veza_db")
    • Après: DatabaseURL: getEnvRequired("DATABASE_URL")
  • internal/config/config_test.go (nouveaux tests ajoutés)

    • Ajout de TestNewConfig_RequiresJWTSecret() (ligne 287)
    • Ajout de TestNewConfig_RequiresDatabaseURL() (ligne 310)
  • cmd/migrate_tool/main.go (lignes 16-20)

    • Avant: Password: getEnv("DB_PASSWORD", "veza")
    • Après: Password: getEnvRequired("DB_PASSWORD")
    • Ajout de la fonction getEnvRequired() dans ce fichier
  • .env.example (nouveau fichier créé)

    • Documentation complète des variables d'environnement
    • JWT_SECRET et DATABASE_URL marqués comme REQUIS

Fichiers analysés (non modifiés)

  • internal/config/config.go - Fonction Load() utilise déjà getEnvRequired()
  • internal/services/jwt_service.go - Gère correctement l'absence de secret
  • internal/config/secrets.go - Liste des secrets correctement définie

2. Autres secrets avec défaut dangereux trouvés

Variable Fichier Action Statut
JWT_SECRET internal/config/config.go:116 Remplacé par getEnvRequired() CORRIGÉ
DATABASE_URL internal/config/config.go:122 Remplacé par getEnvRequired() (contient password) CORRIGÉ
DB_PASSWORD cmd/migrate_tool/main.go:20 Remplacé par getEnvRequired() CORRIGÉ
DB_PASSWORD (test) internal/database/pool_test.go:23,86 Acceptable (fichier de test uniquement) OK

Variables avec défaut acceptable (gardées)

Variable Fichier Justification
PORT config.go:113 Valeur par défaut "8080" acceptable pour dev local
LOG_LEVEL config.go:110 Valeur par défaut "INFO" acceptable
REDIS_URL config.go:121 URL locale par défaut acceptable pour dev
CORS_ORIGINS config.go:101 Défaut "*" acceptable pour dev local
CHAT_JWT_SECRET config.go:120 Fallback vers JWT_SECRET (maintenant requis)

3. Code du fix

3.1 Fonction getEnvRequired() (déjà existante)

// getEnvRequired récupère une variable d'environnement requise (panique si absente)
func getEnvRequired(key string) string {
	value := os.Getenv(key)
	if value == "" {
		panic(fmt.Sprintf("Required environment variable %s is not set", key))
	}
	return value
}

3.2 Modification dans NewConfig()

AVANT (ligne 116):

jwtSecret := getEnv("JWT_SECRET", "your-super-secret-jwt-key")

APRÈS (ligne 115-116):

// SECURITY: JWT_SECRET est REQUIS - pas de valeur par défaut pour éviter les failles de sécurité
jwtSecret := getEnvRequired("JWT_SECRET")

AVANT (ligne 122):

DatabaseURL: getEnv("DATABASE_URL", "postgresql://veza:password@localhost:5432/veza_db"),

APRÈS (ligne 122-123):

// SECURITY: DATABASE_URL est REQUIS - contient des credentials sensibles
DatabaseURL: getEnvRequired("DATABASE_URL"),

3.3 Correction dans cmd/migrate_tool/main.go

AVANT:

Password: getEnv("DB_PASSWORD", "veza"),

APRÈS:

// SECURITY: DB_PASSWORD is required - no default value to prevent security issues
dbPassword := getEnvRequired("DB_PASSWORD")
// ...
Password: dbPassword,

Avec ajout de la fonction getEnvRequired() dans ce fichier.


4. Tests ajoutés

4.1 Test pour JWT_SECRET manquant

// TestNewConfig_RequiresJWTSecret vérifie que NewConfig() refuse de démarrer sans JWT_SECRET
// Ce test valide la correction de sécurité qui empêche l'utilisation d'une valeur par défaut hardcodée
func TestNewConfig_RequiresJWTSecret(t *testing.T) {
	// Sauvegarder les valeurs originales
	originalJWTSecret := os.Getenv("JWT_SECRET")
	originalDatabaseURL := os.Getenv("DATABASE_URL")

	// Nettoyer après le test
	defer func() {
		if originalJWTSecret != "" {
			os.Setenv("JWT_SECRET", originalJWTSecret)
		} else {
			os.Unsetenv("JWT_SECRET")
		}
		if originalDatabaseURL != "" {
			os.Setenv("DATABASE_URL", originalDatabaseURL)
		} else {
			os.Unsetenv("DATABASE_URL")
		}
	}()

	// Supprimer JWT_SECRET - devrait causer un panic
	os.Unsetenv("JWT_SECRET")
	// Définir DATABASE_URL pour éviter un panic sur cette variable (on teste seulement JWT_SECRET)
	os.Setenv("DATABASE_URL", "postgresql://test:test@localhost:5432/test_db")

	// Devrait paniquer car JWT_SECRET est requis
	assert.Panics(t, func() {
		_, _ = NewConfig()
	}, "NewConfig should panic when JWT_SECRET is missing")
}

4.2 Test pour DATABASE_URL manquant

// TestNewConfig_RequiresDatabaseURL vérifie que NewConfig() refuse de démarrer sans DATABASE_URL
// Ce test valide la correction de sécurité qui empêche l'utilisation d'une valeur par défaut avec credentials
func TestNewConfig_RequiresDatabaseURL(t *testing.T) {
	// Sauvegarder les valeurs originales
	originalJWTSecret := os.Getenv("JWT_SECRET")
	originalDatabaseURL := os.Getenv("DATABASE_URL")

	// Nettoyer après le test
	defer func() {
		if originalJWTSecret != "" {
			os.Setenv("JWT_SECRET", originalJWTSecret)
		} else {
			os.Unsetenv("JWT_SECRET")
		}
		if originalDatabaseURL != "" {
			os.Setenv("DATABASE_URL", originalDatabaseURL)
		} else {
			os.Unsetenv("DATABASE_URL")
		}
	}()

	// Définir JWT_SECRET (minimum 32 caractères pour passer la validation)
	os.Setenv("JWT_SECRET", "test-jwt-secret-key-minimum-32-characters-long")
	// Supprimer DATABASE_URL - devrait causer un panic
	os.Unsetenv("DATABASE_URL")

	// Devrait paniquer car DATABASE_URL est requis
	assert.Panics(t, func() {
		_, _ = NewConfig()
	}, "NewConfig should panic when DATABASE_URL is missing")
}

4.3 Résultat des tests

$ go test ./internal/config -run TestNewConfig_RequiresJWTSecret -v
=== RUN   TestNewConfig_RequiresJWTSecret
--- PASS: TestNewConfig_RequiresJWTSecret (0.00s)
PASS
ok  	veza-backend-api/internal/config	0.015s

Tests passent avec succès


5. Documentation mise à jour

5.1 Fichier .env.example créé

Nouveau fichier créé : veza-backend-api/.env.example

Contenu clé:

  • Section "VARIABLES REQUISES" avec JWT_SECRET et DATABASE_URL
  • Instructions claires pour générer JWT_SECRET
  • Toutes les variables optionnelles documentées avec leurs valeurs par défaut
  • Commentaires explicatifs pour chaque variable

Extrait:

# ============================================
# VARIABLES REQUISES (DOIVENT ÊTRE DÉFINIES)
# ============================================

# JWT_SECRET - REQUIS - Secret pour signer et valider les tokens JWT
# DOIT être défini - minimum 32 caractères pour la sécurité
# Générer avec: openssl rand -base64 32
JWT_SECRET=

# DATABASE_URL - REQUIS - URL de connexion à la base de données PostgreSQL
# Format: postgresql://user:password@host:port/database?sslmode=disable
# DOIT être défini - contient des credentials sensibles
DATABASE_URL=

5.2 Documentation existante

  • internal/config/docs.go - JWT_SECRET déjà marqué comme Required: true
  • internal/config/docs_test.go - Tests vérifient que JWT_SECRET est requis
  • ⚠️ README principal - Ne mentionne pas les variables d'environnement (non critique)

6. Audit secrets supplémentaires

6.1 Recherche exhaustive effectuée

Commandes exécutées:

grep -r "JWT_SECRET" veza-backend-api/
grep -r "jwt.*secret\|secret.*jwt" veza-backend-api/ -i
grep -r "getEnv.*secret\|getEnv.*JWT" veza-backend-api/ -i
grep -r "your-super-secret" veza-backend-api/ -i
grep -r "password\|secret\|api_key" veza-backend-api/internal/config/ -i

6.2 Résultats de l'audit

Secrets correctement gérés

Secret Fichier Statut
JWT_SECRET internal/config/config.go Corrigé (getEnvRequired)
DATABASE_URL internal/config/config.go Corrigé (getEnvRequired)
DB_PASSWORD cmd/migrate_tool/main.go Corrigé (getEnvRequired)
JWT_SECRET internal/config/Load() Déjà requis (getEnvRequired)
DB_PASSWORD internal/config/Load() Déjà requis (getEnvRequired)

Secrets dans les tests (acceptables)

Secret Fichier Statut
DB_PASSWORD internal/database/pool_test.go OK (fichier de test uniquement)
JWT_SECRET internal/config/testutils.go OK (utilitaire de test)

Secrets correctement masqués dans les logs

  • internal/config/secrets.go - Fonction MaskSecret() implémentée
  • internal/config/config.go:549 - JWT_SECRET masqué dans les logs
  • internal/config/config.go:550 - DATABASE_URL masqué dans les logs

6.3 Aucun secret hardcodé trouvé

Aucune autre valeur par défaut dangereuse trouvée dans le code de production


7. Commandes pour appliquer

7.1 Vérification des modifications

cd veza-backend-api

# Vérifier que le code compile
go build ./internal/config/...

# Exécuter les tests
go test ./internal/config -run TestNewConfig_Requires -v

# Vérifier tous les tests de config
go test ./internal/config/... -v

7.2 Application en production

⚠️ IMPORTANT: Cette correction est BREAKING pour les environnements qui n'ont pas défini JWT_SECRET.

Étapes de déploiement:

  1. Avant le déploiement:

    # Vérifier que JWT_SECRET est défini dans tous les environnements
    echo $JWT_SECRET  # Ne doit pas être vide
    echo $DATABASE_URL  # Ne doit pas être vide
    
  2. Déployer le code:

    git add internal/config/config.go internal/config/config_test.go .env.example cmd/migrate_tool/main.go
    git commit -m "security: Remove hardcoded JWT_SECRET default value
    
    - Replace getEnv() with getEnvRequired() for JWT_SECRET in NewConfig()
    - Replace getEnv() with getEnvRequired() for DATABASE_URL (contains credentials)
    - Add tests to verify panic when required variables are missing
    - Create .env.example with clear documentation of required variables
    - Fix DB_PASSWORD default in migrate_tool
    
    BREAKING CHANGE: JWT_SECRET and DATABASE_URL are now required.
    Application will panic at startup if these variables are not set."
    
    git push
    
  3. Vérifier le démarrage:

    # L'application doit démarrer normalement si les variables sont définies
    # L'application doit PANIC si JWT_SECRET ou DATABASE_URL sont absents
    

7.3 Migration des environnements existants

Pour les environnements qui utilisent encore la valeur par défaut:

  1. Générer un nouveau JWT_SECRET:

    openssl rand -base64 32
    
  2. Définir la variable d'environnement:

    export JWT_SECRET="<valeur-générée>"
    # Ou dans .env:
    echo "JWT_SECRET=<valeur-générée>" >> .env
    
  3. Redémarrer l'application


8. Impact et compatibilité

8.1 Rétrocompatibilité

Rétrocompatible pour les environnements déjà configurés correctement :

  • Si JWT_SECRET est défini → Aucun changement de comportement
  • Si DATABASE_URL est défini → Aucun changement de comportement

Breaking change pour les environnements non configurés :

  • Si JWT_SECRET n'est pas défini → Application panic au démarrage
  • Si DATABASE_URL n'est pas défini → Application panic au démarrage

8.2 Message d'erreur

En cas de variable manquante, l'application affichera :

panic: Required environment variable JWT_SECRET is not set

ou

panic: Required environment variable DATABASE_URL is not set

Avantage: Message clair et explicite, pas de crash silencieux.


9. Validation finale

Checklist de sécurité

  • JWT_SECRET n'a plus de valeur par défaut hardcodée
  • DATABASE_URL n'a plus de valeur par défaut avec credentials
  • DB_PASSWORD dans migrate_tool corrigé
  • Tests ajoutés pour vérifier le comportement
  • Documentation créée (.env.example)
  • Aucun autre secret avec défaut dangereux trouvé
  • Code compile sans erreur
  • Tests passent

Tests de validation

# Test 1: Vérifier que NewConfig() panic sans JWT_SECRET
$ go test ./internal/config -run TestNewConfig_RequiresJWTSecret -v
PASS

# Test 2: Vérifier que NewConfig() panic sans DATABASE_URL
$ go test ./internal/config -run TestNewConfig_RequiresDatabaseURL -v
PASS

# Test 3: Compilation
$ go build ./internal/config/...
OK

10. Conclusion

Faille de sécurité corrigée avec succès

  • 3 fichiers modifiés pour corriger les valeurs par défaut dangereuses
  • 2 nouveaux tests ajoutés pour valider le comportement
  • 1 fichier de documentation créé (.env.example)
  • Aucun secret hardcodé restant dans le code de production

L'application refuse maintenant de démarrer si JWT_SECRET ou DATABASE_URL ne sont pas définis, empêchant ainsi l'utilisation accidentelle de valeurs par défaut non sécurisées.


Rapport généré le: 2025-01-27
Validé par: Tests automatisés