Le codebase VEZA est **globalement bien sécurisé** avec une architecture de sécurité mature (middleware chain, RBAC, token versioning, CSRF, rate limiting multi-couche). Les vulnérabilités critiques identifiées dans l'audit précédent (mars 2026) ont été corrigées (VEZA-SEC-001 JWT secret par défaut, VEZA-SEC-002 issuer/audience mismatch).
| HAUTE | 10 | Race conditions marketplace (downloads, promo codes, licence exclusive), production HS256, IP spoofing rate limiter (pas de TrustedProxies), RGPD hard delete incomplet, RTMP callback auth faible, co-écoute host hijack, modérateur self-strike, free trial illimité |
| MOYENNE | 12 | Recovery codes math/rand, metrics IP spoofing, ClamAV latest, pagination sans limit max, WebSocket pas de re-auth, CSP Swagger, CI actions non-pinnées, RabbitMQ mgmt, password reset no rate limit, access token in response body, email dans logs, analytics sans k-anonymité |
| BASSE | 6 | Password policy mismatch FE/BE, Hyperswitch version, dotenv crate, Elasticsearch sans auth, context.Background dans workers, Redis sans auth dev |
Les endpoints `GET /api/v1/conversations/:id` et `GET /api/v1/conversations/:id/history` ne vérifient **pas** que l'utilisateur authentifié est membre de la room avant de retourner les données. N'importe quel utilisateur authentifié peut lire l'intégralité des messages de n'importe quelle conversation en connaissant son UUID.
**Preuve de concept**
```bash
# L'utilisateur B (non membre) accède à la conversation privée de l'utilisateur A
Le modèle `Track` expose `play_count` et `like_count` dans les réponses JSON via les tags `json:"play_count"` et `json:"like_count"`. Ces données sont retournées dans toutes les réponses de l'API publique (liste de tracks, détail de track, recherche).
Cela viole la **règle immuable #4 du CLAUDE.md** : *"JAMAIS de métriques de popularité publiques — Les likes et play counts sont PRIVÉS (visibles uniquement par le créateur dans ses analytics)"*.
**Impact**
- Violation du principe éthique fondamental de VEZA (pas de dark patterns FOMO)
- Crée un biais de popularité dans la découverte musicale
- Discrimine les artistes émergents
**Remédiation**
```go
// Créer un type de réponse publique sans les métriques
type TrackPublicResponse struct {
ID uuid.UUID `json:"id"`
Title string `json:"title"`
ArtistID uuid.UUID `json:"artist_id"`
// PAS de PlayCount ni LikeCount
}
// N'exposer play_count et like_count que dans /api/v1/me/tracks/:id/analytics (creator only)
```
**Priorité de correction** : Immédiate (bloquant v1.0.0)
La fonction `GetDownloadURL` vérifie d'abord qu'une licence existe avec `downloads_left > 0` (ligne 794), puis décrémente `downloads_left` séparément (ligne 817). Ces deux opérations ne sont pas atomiques et ne sont pas dans une transaction avec verrouillage.
**Preuve de concept**
```
# Deux requêtes simultanées avec downloads_left = 1
Un acheteur peut contourner la limite de téléchargements d'une licence en envoyant plusieurs requêtes simultanées. Impact financier direct pour les créateurs vendant des licences exclusives à téléchargements limités.
**Remédiation**
```go
// Utiliser une transaction avec SELECT FOR UPDATE
tx := s.db.Begin()
err := tx.Set("gorm:query_option", "FOR UPDATE").
Where("buyer_id = ? AND product_id = ? AND downloads_left > 0 AND revoked_at IS NULL", buyerID, productID).
Le `docker-compose.prod.yml` configure `JWT_SECRET` (ligne 158) mais pas `JWT_PRIVATE_KEY_PATH` ni `JWT_PUBLIC_KEY_PATH`. Le code Go préfère RS256 si les clés sont fournies, sinon fallback HS256. En production, c'est HS256 qui est utilisé.
HS256 utilise un secret partagé symétrique — si le secret est compromis (leak env var, backup, log), un attaquant peut forger des tokens valides. RS256 nécessite la clé privée pour signer mais la clé publique suffit pour vérifier — meilleure séparation des responsabilités.
**Impact**
Compromission du JWT_SECRET = compromission de tous les comptes utilisateurs. Avec RS256, seul le serveur ayant la clé privée peut signer des tokens.
**Remédiation**
1. Générer une paire RSA 2048-bit pour la production
2. Ajouter dans `docker-compose.prod.yml` :
```yaml
- JWT_PRIVATE_KEY_PATH=/secrets/jwt_private.pem
- JWT_PUBLIC_KEY_PATH=/secrets/jwt_public.pem
```
3. Monter les clés via Docker secrets ou volume sécurisé
4. Supprimer `JWT_SECRET` de la config production
5. Invalider tous les tokens existants (incrémenter token_version globalement)
**Priorité de correction** : Immédiate (avant v1.0.0)
---
### [HIGH-003] : User repository bypass du context de requête
Six méthodes du UserRepository (`GetByID`, `GetByEmail`, `GetByUsername`, `Create`, `Update`, `Delete`) utilisent `context.Background()` au lieu de propager le context de la requête HTTP. Cela signifie que :
1. Les timeouts de requête ne s'appliquent pas aux opérations DB sous-jacentes
2. L'annulation de la requête (client disconnect) ne cancelle pas la query DB
3. Un attaquant peut déclencher des requêtes DB longues qui ne seront pas annulées
**Impact**
Sous charge, des requêtes lentes peuvent s'accumuler car elles ne sont pas annulées quand le client se déconnecte. Cela peut mener à une saturation du pool de connexions DB (DoS).
La validation du code promo (`validatePromoCodeTx`) vérifie `UsedCount >= MaxUses` puis incrémente `used_count` séparément. Sans `SELECT FOR UPDATE`, deux requêtes concurrentes peuvent utiliser le même code promo au-delà de sa limite.
**Remédiation**
Ajouter `clause.Locking{Strength: "UPDATE"}` dans la requête de validation du code promo.
**Priorité de correction** : Sprint suivant
---
### [HIGH-005] : Pas de protection race condition sur licences exclusives
Aucune vérification atomique n'empêche deux acheteurs d'acheter simultanément une licence exclusive du même produit. Deux transactions peuvent créer deux licences exclusives pour le même track.
**Remédiation**
Ajouter un check `SELECT FOR UPDATE` dans la transaction de création de licence :
```go
var existing License
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("product_id = ? AND type = 'exclusive' AND revoked_at IS NULL", prod.ID).
First(&existing).Error; err == nil {
return ErrExclusiveLicenseAlreadySold
}
```
**Priorité de correction** : Sprint suivant
---
### [HIGH-006] : Rate limiter bypass — TrustedProxies non configuré
Gin's `c.ClientIP()` fait confiance à `X-Forwarded-For` automatiquement. Sans appel explicite à `engine.SetTrustedProxies()`, n'importe quel client peut forger son IP et bypasser tous les rate limiters (DDoS, brute force, upload).
**Remédiation**
```go
engine.SetTrustedProxies([]string{"127.0.0.1", "10.0.0.0/8"}) // Adapter au proxy réel
Le hard delete worker anonymise uniquement `users` et `user_profiles` en DB. Les données PII restent dans : Redis (sessions/cache), Elasticsearch (index de recherche), audit_logs (user_id, IP), RabbitMQ (messages en queue).
**Remédiation**
Étendre le hard delete worker pour nettoyer : Redis keys, ES index, audit_logs anonymisation, RabbitMQ purge.
La fonction `validateCallbackSecret()` fallback à `c.Query("name")` quand le header `X-RTMP-Callback-Secret` est absent. Le paramètre "name" contient en réalité le stream_key de l'utilisateur, pas le secret. Cela permet à quiconque connaissant un stream_key de déclencher des callbacks publish/publish_done.
De plus, si `RTMP_CALLBACK_SECRET` n'est pas configuré, la validation retourne `true` (fail-open).
**Remédiation**
- Supprimer le fallback à `c.Query("name")`
- Retourner `false` si le secret n'est pas configuré (fail-closed)
- Utiliser `hmac.Equal()` pour la comparaison
**Priorité de correction** : Sprint suivant
---
### [HIGH-009] : Co-écoute — participant non-host peut prendre le contrôle
Le flag `IsHost` est défini à la connexion mais n'est pas re-vérifié lors du traitement des messages. Un listener peut envoyer un message `UpdateHostState` et son état de lecture sera broadcast comme autoritaire à tous les participants.
**Remédiation**
Vérifier `conn.IsHost` avant de traiter les messages de type `UpdateHostState`/`SyncClientStateMsg`.
**Priorité de correction** : Sprint suivant
---
### [HIGH-010] : Modérateur peut émettre des strikes sans contrôle de conflit d'intérêts
Aucune vérification n'empêche un modérateur d'émettre un strike à lui-même ou de résoudre son propre appel. Un modérateur malveillant pourrait abuser du système pour cibler des utilisateurs spécifiques.
Les codes de récupération 2FA sont générés avec `mathrand.Intn()` (math/rand) au lieu de `crypto/rand`. `math/rand` utilise un PRNG prédictible — si un attaquant connaît le seed ou peut observer suffisamment de sorties, il peut prédire les codes de récupération futurs.
Depuis Go 1.20, `math/rand` auto-seed est non déterministe, réduisant le risque. Cependant, les standards cryptographiques exigent `crypto/rand` pour tout matériel lié à l'authentification.
**Impact**
Prédiction théorique des codes de récupération 2FA, permettant le contournement de l'authentification à deux facteurs.
Le middleware de protection des métriques lit `X-Forwarded-For` directement depuis la requête et l'utilise pour vérifier l'IP, remplaçant `c.ClientIP()`. Un attaquant peut forger ce header pour bypasser le whitelist IP.
```go
// Ligne 52-54 — X-Forwarded-For forgeable
clientIP := c.ClientIP()
if forwarded := c.GetHeader("X-Forwarded-For"); forwarded != "" {
Information disclosure : un attaquant peut accéder aux métriques Prometheus (pool DB, error rates, endpoints lents, charge système) pour préparer une attaque ciblée.
**Remédiation**
Supprimer la lecture de `X-Forwarded-For` dans ce middleware. Utiliser uniquement `c.ClientIP()` qui est déjà configuré pour tenir compte du proxy de confiance (via `gin.SetTrustedProxies`).
```go
// Supprimer les lignes 52-55, utiliser uniquement :
clientIP := c.ClientIP()
```
**Priorité de correction** : Sprint suivant
---
### [MEDIUM-003] : Image Docker ClamAV non pinnée (:latest)
L'image ClamAV utilise le tag `:latest` dans les deux fichiers docker-compose (dev ET production). Cela expose à :
1. Des breaking changes non testées
2. Un potentiel supply chain attack (image compromise)
3. Des builds non reproductibles
**Impact**
Un changement dans l'image ClamAV pourrait casser silencieusement le scan antivirus des uploads, soit en désactivant la protection, soit en bloquant tous les uploads.
**Remédiation**
Pinner l'image à un tag de version spécifique :
```yaml
clamav:
image: clamav/clamav:1.4.2 # Vérifier la dernière version stable
```
**Priorité de correction** : Sprint suivant
---
### [MEDIUM-004] : Pagination sans limite maximale explicite
L'audit précédent signalait l'absence de limite maximale explicite sur la pagination. Si un utilisateur envoie `?limit=100000`, le serveur pourrait retourner une réponse massive consommant mémoire et bande passante.
**Impact**
Un attaquant peut envoyer des requêtes avec un `limit` très élevé pour surcharger le serveur (DoS). Impact amplifié si la requête joint plusieurs tables.
**Remédiation**
Ajouter un cap sur le paramètre `limit` dans le middleware de pagination :
```go
const MaxPageSize = 100
if limit > MaxPageSize {
limit = MaxPageSize
}
```
**Priorité de correction** : Sprint suivant
---
### [MEDIUM-005] : Production JWT utilise HS256 — stream token potentiellement forgeable si secret compromis
Le stream token (JWT court, 5 min TTL) pour l'authentification HLS utilise le même secret/clé que le JWT principal. Avec HS256 en production, le compromis du `JWT_SECRET` permet de forger des stream tokens pour accéder à n'importe quel contenu audio.
Le stream token utilise des claims différents (`iss: veza-platform`, `aud: veza-services`) mais la même clé de signature.
**Impact**
Accès non autorisé au streaming audio de tous les contenus de la plateforme.
**Remédiation**
Migrer vers RS256 (cf. HIGH-002). Alternativement, utiliser un secret séparé pour les stream tokens.
**Priorité de correction** : Sprint suivant (résolu par HIGH-002)
---
### [MEDIUM-006] : CSP unsafe-inline et unsafe-eval pour les routes Swagger
Les routes Swagger (`/swagger/`, `/docs/`) ont un CSP permissif avec `'unsafe-inline'` et `'unsafe-eval'` pour le JavaScript. Bien que nécessaire pour le fonctionnement de Swagger UI, cela crée un vecteur XSS si un attaquant peut injecter du contenu dans la documentation Swagger.
**CWE** : CWE-829 — Inclusion of Functionality from Untrusted Control Sphere
**OWASP** : A08:2021 — Software and Data Integrity Failures
**Fichier(s)** : `.github/workflows/ci.yml` (et tous les workflows)
**Composant** : CI/CD — GitHub Actions
**Description**
Les actions GitHub sont référencées par tag mutable (`@v4`, `@v5`, `@stable`) au lieu de SHA de commit. Un tag peut être déplacé par le mainteneur de l'action, ou en cas de compromission du compte du mainteneur.
Un attaquant compromettant un mainteneur d'action GitHub pourrait injecter du code malveillant dans le pipeline CI (exfiltration de secrets, backdoor dans les artifacts de build).
**CWE** : CWE-668 — Exposure of Resource to Wrong Sphere
**OWASP** : A05:2021 — Security Misconfiguration
**Fichier(s)** : `docker-compose.yml:91`
**Composant** : Infrastructure — RabbitMQ
**Description**
Le Management UI de RabbitMQ (port 25672→15672) est exposé sur l'hôte en dev. En production (`docker-compose.prod.yml`), les ports ne sont pas exposés directement, mais le service utilise toujours l'image `rabbitmq:3-management-alpine` qui inclut le plugin management.
**Impact**
En dev : accès au dashboard RabbitMQ exposant la topologie des queues et les messages. En prod : si un attaquant accède au réseau Docker, il peut accéder au management UI.
**Remédiation**
- Prod : utiliser `rabbitmq:3-alpine` (sans management) ou désactiver le plugin management
Le code accorde un essai gratuit si `plan.TrialDays > 0` sans vérifier si l'utilisateur a déjà bénéficié d'un essai. Un utilisateur peut cycler entre free et premium pour obtenir des essais illimités.
**Remédiation**
Vérifier en DB si l'utilisateur a déjà eu un trial pour ce plan avant d'en accorder un nouveau.
**Priorité de correction** : Sprint suivant
---
### [MEDIUM-010] : WebSocket — pas de re-validation token après connexion
Le token JWT est validé uniquement à l'upgrade WebSocket. Si le token est révoqué (logout, ban), l'utilisateur reste connecté et peut continuer à envoyer/recevoir des messages.
**Remédiation**
Implémenter une re-validation périodique du token (toutes les 30 secondes) ou écouter les événements de révocation via Redis PubSub.
**Priorité de correction** : Sprint suivant
---
### [MEDIUM-011] : Email logué en clair dans les tentatives de login
L'email de l'utilisateur est logué en clair lors des tentatives de login. Le `SecretFilterCore` filtre les champs `password`, `token`, `secret` mais pas `email`.
**Remédiation**
Ajouter `email` à la liste de redaction dans `secret_filter.go`, ou logger un hash de l'email.
**Priorité de correction** : Backlog
---
### [MEDIUM-012] : Analytics créateur sans k-anonymité (minimum 10 utilisateurs)
Les analytics créateur affichent des statistiques détaillées (heatmap, écoutes, géographie) même si un track n'a que 1-2 écoutes. Cela pourrait permettre de ré-identifier des auditeurs par recoupement.
**Remédiation**
Supprimer les analytics détaillées si `unique_listeners < 10`. Afficher uniquement des catégories agrégées.
**OWASP** : A07:2021 — Identification and Authentication Failures
**ASVS** : V2.1.1
**Fichier(s)** : `apps/web/src/lib/passwordValidator.ts` vs `veza-backend-api/internal/validators/password_validator.go`
**Composant** : Authentication — Password Policy
**Description**
L'audit précédent (VEZA-SEC-005) signalait que le frontend accepte 8 caractères alors que le backend exige 12. Si non corrigé, l'utilisateur reçoit une erreur serveur après avoir rempli le formulaire — mauvaise UX.
L'image Hyperswitch `juspaydotin/hyperswitch-router:2025.01.21.0-standalone` date de janvier 2025 (~14 mois). Des correctifs de sécurité et des améliorations de stabilité ont pu être publiés depuis.
**Remédiation**
Mettre à jour vers la dernière version stable d'Hyperswitch après test en staging.
**CWE** : CWE-306 — Missing Authentication for Critical Function
**OWASP** : A07:2021 — Identification and Authentication Failures
**Fichier(s)** : `docker-compose.yml` (Elasticsearch si présent), configuration ES
**Composant** : Infrastructure — Elasticsearch
**Description**
Elasticsearch n'a pas d'authentification configurée dans les docker-compose. Tout service du réseau Docker peut accéder aux index et requêter les données directement.
**Impact**
Si un autre container est compromis, l'attaquant peut accéder aux index Elasticsearch contenant potentiellement des données utilisateur (tracks, descriptions, messages indexés).
**Remédiation**
Activer l'authentification native Elasticsearch ou OpenSearch Security.
**Priorité de correction** : Backlog
---
### [LOW-005] : context.Background() dans les background jobs
**Sévérité** : BASSE
**CVSS v3.1** : 2.0
**CWE** : CWE-404 — Improper Resource Shutdown or Release
Les jobs de nettoyage (cleanup_password_reset_tokens, cleanup_sessions, cleanup_hls_segments, cleanup_verification_tokens) utilisent `context.Background()`. Cela est acceptable pour des background jobs, mais ils devraient utiliser un context dérivé du shutdown manager pour permettre un arrêt gracieux.
**Remédiation**
Passer un context annulable depuis le shutdown manager.
**Priorité de correction** : Backlog
---
### [LOW-006] : Redis sans mot de passe en développement
Redis en dev n'a pas de `requirepass`. Acceptable en développement local, mais le docker-compose.yml de base ne devrait pas être utilisé tel quel en staging/pré-production.
**Impact**
Limité au développement local. La config production (`docker-compose.prod.yml:34`) utilise bien `requirepass`.
**Remédiation**
OK en l'état (déjà protégé en prod). Documenter que `docker-compose.yml` est strictement pour le dev local.
**Priorité de correction** : Backlog
---
### [INFO-001] : Bonnes pratiques de sécurité confirmées
**Sévérité** : INFO (Positif)
Les contrôles de sécurité suivants sont correctement implémentés :
1.**JWT Token Versioning** : Révocation immédiate via `token_version` vérifié contre la DB à chaque requête (`auth.go:218`)
2.**Token Blacklist Redis** : Double vérification avec blacklist Redis quand disponible (`auth.go:189`)
4.**Security Headers** : Ensemble complet (HSTS, CSP strict pour API, X-Frame-Options DENY, nosniff, Referrer-Policy, Permissions-Policy) (`security_headers.go`)
5.**CORS Strict** : Production requiert whitelist explicite, panic si non configuré (`cors.go`)
6.**CSRF Protection** : Middleware CSRF avec tokens Redis (`csrf.go`)
7.**Rate Limiting Multi-couche** : Global, par IP, par endpoint, par user, upload-specific (`rate_limiter.go`, `ratelimit_redis.go`, `endpoint_limiter.go`)
8.**ClamAV Obligatoire** : `CLAMAV_REQUIRED=true` en production (`docker-compose.prod.yml:170`)
9.**Bcrypt Cost 12** : Conforme aux recommandations OWASP
10.**Path Validation** : `ValidateExecPath()` sur tous les appels `exec.Command` (`utils/sanitizer.go`)
11.**Secret Filtering** : Logs filtrés pour les secrets (`logging/secret_filter.go`)
12.**Audit Trail** : Middleware d'audit sur POST/PUT/DELETE (`audit.go`)
13.**RBAC Middleware** : Contrôle d'accès basé sur les rôles avec middleware dédié (`rbac_middleware.go`)
14.**Ownership Checks** : `RequireOwnershipOrAdmin` sur les ressources utilisateur
15.**Webhook HMAC-SHA512** : Vérification de signature avec comparaison constant-time
16.**Non-root Docker** : Container backend tourne en user `app:1001`
17.**Multi-stage Dockerfile** : Build optimisé sans outils de compilation dans l'image finale
18.**GORM** : Pas de SQL injection — toutes les queries production utilisent des paramètres préparés
19.**DOMPurify** : Sanitisation HTML côté frontend avec whitelist de tags
20.**User Enumeration Protection** : Messages d'erreur génériques sur login (pas de "user not found" vs "wrong password")
---
### [INFO-002] : Vérification des correctifs de l'audit précédent
| VEZA-SEC-003 (Shutdown AppState Rust) | ⚠️ À VÉRIFIER | Non vérifié dans cet audit |
| VEZA-SEC-004 (Webhook worker goroutine) | ⚠️ À VÉRIFIER | Non vérifié dans cet audit |
| VEZA-SEC-005 (Password policy mismatch) | ❌ NON CORRIGÉ | Repris comme LOW-001 |
| VEZA-SEC-006 (Métriques exposées) | ✅ CORRIGÉ | `metrics_protection.go` avec bearer token + IP whitelist |
| VEZA-SEC-007 (context.Background password_reset) | ⚠️ À VÉRIFIER | Non vérifié dans cet audit |
| VEZA-SEC-008 (Callback no-op seller) | ⚠️ À VÉRIFIER | Non vérifié dans cet audit |
| VEZA-SEC-009 (dotenv obsolète) | ❌ NON CORRIGÉ | Repris comme LOW-003 |
---
### [INFO-003] : Recommandation — Ajouter un scan de dépendances dans le pipeline
**Sévérité** : INFO
Le CI inclut déjà `govulncheck`, `cargo audit`, et `npm audit`. Recommandation supplémentaire :
- Ajouter `npm audit --production` (exclure les devDeps)
- Ajouter un check de licence automatisé (ex: `license-checker` pour npm, `golicense` pour Go)
- Configurer Dependabot pour les GitHub Actions elles-mêmes
---
### [INFO-004] : Recommandation — Tests de sécurité automatisés
**Sévérité** : INFO
Des tests de sécurité existent (`tests/security/`, `tests/integration/webhook_security_test.go`). Recommandation :
- Ajouter des tests de race condition automatisés pour les scénarios marketplace (HIGH-001)
- Ajouter un test DAST automatisé (OWASP ZAP) dans le pipeline CI/CD
- Ajouter un test de politique CSP (vérifier que `unsafe-inline` n'apparaît pas sur les routes non-Swagger)
---
### [INFO-005] : Aucun `unsafe` dans le code Rust
**Sévérité** : INFO (Positif)
Aucun bloc `unsafe` trouvé dans le code applicatif du stream server Rust. Le code utilise exclusivement les abstractions safe de Rust, éliminant les risques de corruption mémoire.