veza/docs/PENTEST_REPORT_VEZA_v0.12.6.md

398 lines
16 KiB
Markdown
Raw Normal View History

# Rapport de Pentest — VEZA v0.12.6
**Date** : 2026-03-13
**Auditeur** : Claude Opus 4.6 (SAST + revue manuelle de code + 6 agents d'audit spécialisés)
**Scope** : Monorepo complet — Go backend, Rust stream server, React frontend, CI/CD, infrastructure Docker
**Méthodologie** : OWASP Top 10 2021, OWASP API Security Top 10 2023, ASVS Level 2
**Référence** : ORIGIN_SECURITY_FRAMEWORK.md
---
## Résumé Exécutif
| Sévérité | Count |
|----------|-------|
| CRITICAL | 5 |
| HIGH | 10 |
| MEDIUM | 12 |
| LOW | 6 |
| INFO | 3 |
| **Total findings** | **36** |
| **PASS (contrôles validés)** | **24** |
**Posture globale** : La codebase VEZA présente une architecture sécurisée avec d'excellentes fondations (JWT RS256, bcrypt, HMAC webhooks, rate limiting Redis). Cependant, l'audit approfondi révèle 5 vulnérabilités critiques — principalement des race conditions financières, des IDOR sur les analytics, et des WebSocket sans validation d'origine. Ces issues sont toutes remédiables sans refonte architecturale.
---
## CRITICAL FINDINGS
### CRIT-001 — WebSocket Cross-Site Hijacking (3 handlers)
| Champ | Valeur |
|-------|--------|
| **Sévérité** | CRITICAL |
| **OWASP** | A07:2021 — Cross-Site Request Forgery |
| **CWE** | CWE-346 (Origin Validation Error) |
| **Fichiers** | `chat_websocket_handler.go:50`, `playback_websocket_handler.go:106`, `co_listening_websocket_handler.go:105` |
| **CVSS** | 8.1 |
**Description** : Les 3 handlers WebSocket utilisent `InsecureSkipVerify: true` dans `websocket.AcceptOptions`, désactivant la validation du header Origin. Un attaquant peut établir une connexion WebSocket depuis n'importe quel domaine et usurper les sessions utilisateur.
```go
conn, err := websocket.Accept(c.Writer, c.Request, &websocket.AcceptOptions{
InsecureSkipVerify: true, // VULNÉRABLE
})
```
**Impact** : Cross-Site WebSocket Hijacking (CSWSH) — un attaquant peut intercepter les messages chat, contrôler la lecture audio, et accéder aux sessions d'écoute collaborative.
**Remédiation** : Supprimer `InsecureSkipVerify: true`, implémenter `OriginPatterns` avec whitelist des domaines autorisés.
---
### CRIT-002 — Payout Balance Race Condition (Double-Spend)
| Champ | Valeur |
|-------|--------|
| **Sévérité** | CRITICAL |
| **OWASP** | API6:2023 — Unrestricted Access to Sensitive Business Flows |
| **CWE** | CWE-362 (Race Condition) |
| **Fichier** | `veza-backend-api/internal/core/marketplace/payout.go:175-195` |
| **CVSS** | 9.1 |
**Description** : `RequestPayout()` lit le solde hors transaction (`GetSellerBalance`), puis ouvre une transaction pour déduire. Entre la lecture et la déduction, une requête concurrente peut aussi lire le même solde et traiter un deuxième payout.
**Scénario d'attaque** : Seller avec $100 → 2 requêtes simultanées → les 2 lisent $100 → les 2 traitent $100 → **$200 payés pour $100 de solde**.
**Remédiation** : Ajouter `SELECT FOR UPDATE` (`clause.Locking{Strength: "UPDATE"}`) sur la ligne de balance DANS la transaction, avant la validation du montant.
---
### CRIT-003 — Refund Double-Pay Race Condition
| Champ | Valeur |
|-------|--------|
| **Sévérité** | CRITICAL |
| **CWE** | CWE-362 |
| **Fichier** | `marketplace/service.go:1136-1189` |
| **CVSS** | 8.7 |
**Description** : `RefundOrder()` lit l'order hors transaction, vérifie l'autorisation, puis appelle `rp.Refund()`. Deux requêtes concurrentes peuvent toutes les deux passer la validation et déclencher un double remboursement.
**Remédiation** : Wrapper la lecture + validation + refund dans une seule transaction avec `SELECT FOR UPDATE` sur l'order. Vérifier `order.Status != "refunded"` atomiquement.
---
### CRIT-004 — Track Analytics IDOR (No Ownership Check)
| Champ | Valeur |
|-------|--------|
| **Sévérité** | CRITICAL |
| **OWASP** | API1:2023 — Broken Object Level Authorization |
| **CWE** | CWE-639 (Authorization Bypass) |
| **Fichier** | `analytics/handler.go:727-777` |
| **CVSS** | 7.5 |
**Description** : `GET /api/v1/analytics/tracks/:id` retourne les analytics détaillées (play count, completion rate, listener data) de n'importe quelle track sans vérifier que l'utilisateur authentifié en est le créateur.
**Impact** : Tout utilisateur authentifié peut espionner les analytics privées de n'importe quel créateur.
**Remédiation** : Ajouter une vérification `track.CreatorID == userID` avant de retourner les analytics.
---
### CRIT-005 — Path Traversal in Marketplace Preview Upload
| Champ | Valeur |
|-------|--------|
| **Sévérité** | CRITICAL |
| **CWE** | CWE-22 (Path Traversal) |
| **Fichier** | `handlers/marketplace.go:285-290` |
| **CVSS** | 8.8 |
**Description** : Le nom de fichier uploadé est utilisé directement dans `filepath.Join` sans sanitization :
```go
destPath := filepath.Join(previewDir, file.Filename) // VULNÉRABLE
```
Un attaquant peut uploader un fichier avec `../../../etc/cron.d/evil` pour écrire n'importe où sur le filesystem.
**Remédiation** : Utiliser `filepath.Base(file.Filename)` ou générer un UUID comme nom de fichier.
---
## HIGH FINDINGS
### HIGH-001 — IP Spoofing via X-Forwarded-For (Rate Limit Bypass)
| Champ | Valeur |
|-------|--------|
| **Sévérité** | HIGH |
| **OWASP** | API4:2023 — Unrestricted Resource Consumption |
| **Fichier** | `handlers/common.go:601-610` |
| **CVSS** | 7.5 |
**Description** : `GetClientIP()` lit directement `X-Forwarded-For` sans passer par `c.ClientIP()` de Gin. Permet le contournement du rate limiting.
**Remédiation** : Remplacer par `c.ClientIP()` + configurer `SetTrustedProxies()`.
---
### HIGH-002 — Métriques de popularité exposées dans l'API publique
| Champ | Valeur |
|-------|--------|
| **Sévérité** | HIGH |
| **Fichiers** | `user_service.go:75`, `social_service.go:321`, `types/stats.go:31`, `social/models.go:32` |
**Description** : `followers_count`, `following_count` exposés dans les réponses publiques (`PublicUserResponse`, `SuggestionUser`). `like_count` exposé sur le modèle `Post` social.
**Remédiation** : Supprimer ces champs des réponses publiques. Les exposer uniquement dans le dashboard privé.
---
### HIGH-003 — Mass Assignment / Privilege Escalation (UpdateUser)
| Champ | Valeur |
|-------|--------|
| **Sévérité** | HIGH |
| **CWE** | CWE-915 (Mass Assignment) |
| **Fichier** | `api/user/handler.go:75-95`, `api/user/service.go:188-267` |
**Description** : `UpdateMe()` appelle `UpdateUser()` qui accepte des champs `Role`, `IsVerified`, `IsActive`. Un utilisateur non-admin pourrait escalader ses privilèges en envoyant `{"role": "admin"}`.
**Remédiation** : Filtrer les champs autorisés par rôle. Seuls les admins peuvent modifier `role`, `is_verified`, `is_active`.
---
### HIGH-004 — RTMP Callback Authentication Faible
| Champ | Valeur |
|-------|--------|
| **Sévérité** | HIGH |
| **CWE** | CWE-345 |
| **Fichier** | `handlers/live_stream_callback.go:26-84` |
**Description** : Les callbacks Nginx-RTMP utilisent un simple header secret statique (`X-RTMP-Callback-Secret`) sans HMAC, sans timestamp, sans protection contre le replay.
**Remédiation** : Implémenter HMAC-SHA256 avec timestamp + body dans la signature.
---
### HIGH-005 — Refresh Token TOCTOU Race Condition
| Champ | Valeur |
|-------|--------|
| **Sévérité** | HIGH |
| **CWE** | CWE-362 |
| **Fichier** | `auth/service.go:653-707` |
**Description** : Entre `Validate()` et `Rotate()` du refresh token, une requête concurrente peut réutiliser le même token. Le `RefreshLock` (line 665-671) réduit la fenêtre mais ne l'élimine pas.
**Remédiation** : Combiner validate + rotate dans une seule transaction DB atomique.
---
### HIGH-006 — Promo Code Counter Race Condition
| Champ | Valeur |
|-------|--------|
| **Sévérité** | HIGH |
| **CWE** | CWE-362 |
| **Fichier** | `marketplace/service.go:464, 764-787` |
**Description** : Le `used_count` du promo code est incrémenté APRÈS validation mais sans garantie atomique. Deux requêtes simultanées peuvent dépasser `MaxUses`.
**Remédiation** : Effectuer validation et incrémentation dans la même transaction avec `SELECT FOR UPDATE`.
---
### HIGH-007 — Stream Token Replay Attack (Rust)
| Champ | Valeur |
|-------|--------|
| **Sévérité** | HIGH |
| **CWE** | CWE-433 |
| **Fichier** | `veza-stream-server/src/auth/token_validator.rs:100-138` |
**Description** : Les URLs de streaming signées avec HMAC-SHA256 vérifient l'expiration mais pas la réutilisation. Un attaquant peut rejouer une URL valide multiple fois avant expiration, gonflant artificiellement les analytics.
**Remédiation** : Ajouter un nonce ou stocker les tokens consommés dans Redis avec TTL.
---
### HIGH-008 — Account Deletion Missing Financial Data Cascade
| Champ | Valeur |
|-------|--------|
| **Sévérité** | HIGH |
| **Fichier** | `handlers/account_deletion_handler.go:75-135` |
**Description** : La suppression de compte anonymise l'utilisateur mais ne traite pas : orders (buyer), licenses, seller payouts, seller balance, seller transfers.
**Remédiation** : Ajouter le nettoyage GDPR pour les données financières (anonymiser plutôt que supprimer pour les obligations légales).
---
### HIGH-009 — Webhook Amount Not Re-Validated
| Champ | Valeur |
|-------|--------|
| **Sévérité** | HIGH |
| **CWE** | CWE-1025 |
| **Fichier** | `marketplace/service.go:610-676` |
**Description** : `ProcessPaymentWebhook()` ne vérifie pas que le montant payé (Hyperswitch) correspond au total de l'order. Un webhook forgé pourrait confirmer un paiement inférieur.
**Remédiation** : Comparer `order.TotalAmount` avec le montant du webhook avant de traiter.
---
### HIGH-010 — Free Trial Reuse Not Atomic
| Champ | Valeur |
|-------|--------|
| **Sévérité** | HIGH |
| **CWE** | CWE-362 |
| **Fichier** | `subscription/service.go:237-251` |
**Description** : La vérification du trial gratuit (`previousTrialCount`) est hors transaction. Deux requêtes concurrentes peuvent toutes les deux voir `count = 0` et obtenir un trial.
**Remédiation** : Déplacer la vérification dans la transaction avec `SELECT FOR UPDATE` sur le user.
---
## MEDIUM FINDINGS
### MEDIUM-001 — Incohérence bcrypt cost (10 vs 12)
**Fichier** : `auth/service.go:156, :986` | `bcrypt.DefaultCost` au lieu de 12
### MEDIUM-002 — GitHub Actions non épinglées par SHA (12+ actions)
**Fichiers** : `sast.yml`, `security-scan.yml`, `cd.yml`, `stream-ci.yml`, `container-scan.yml`
### MEDIUM-003 — CORS .env.production avec HTTP
**Fichier** : `.env.production:40`
### MEDIUM-004 — MinIO bucket publicly readable
**Fichier** : `docker-compose.yml:296``mc anonymous set download veza/veza-files/public`
### MEDIUM-005 — Unbounded pagination (multiple handlers)
**Fichiers** : `api/user/handler.go`, `core/admin/handler.go`, et ~15 autres endpoints sans `max(limit, 100)`
### MEDIUM-006 — File upload size validation missing (marketplace)
**Fichier** : `handlers/marketplace.go:270-289` — pas de vérification `file.Size`
### MEDIUM-007 — Nginx RTMP wildcard CORS
**Fichier** : `infra/nginx-rtmp/nginx.conf:46``Access-Control-Allow-Origin: *`
### MEDIUM-008 — Redis unprotected in development
**Fichier** : `docker-compose.yml:35-56` — pas de `--requirepass` en dev
### MEDIUM-009 — Nginx production missing HSTS header
**Fichier** : `apps/web/nginx.production.conf:11-14`
### MEDIUM-010 — WebSocket message size not limited (playback)
**Fichier** : `playback_websocket_handler.go:138-152` — pas de `conn.SetReadLimit()`
### MEDIUM-011 — License download missing expiration check
**Fichier** : `marketplace/service.go:806-840``expires_at` non vérifié
### MEDIUM-012 — pprof profiling endpoint exposed
**Fichier** : `cmd/api/main.go:8``import _ "net/http/pprof"` sans restriction d'accès
---
## LOW FINDINGS
### LOW-001 — Cookie SameSite=Lax au lieu de Strict
**Fichier** : `config/config.go:138`
### LOW-002 — CASCADE DELETE extensif (30+ migrations)
**Fichier** : `migrations/*.sql`
### LOW-003 — utils.HashPassword utilise bcrypt.DefaultCost
**Fichier** : `utils/utils.go:57`
### LOW-004 — Staging validation workflow actions non épinglées
**Fichier** : `staging-validation.yml:35`
### LOW-005 — Docker base images use :latest (dev Dockerfiles)
**Fichiers** : `veza-backend-api/Dockerfile:28`, `veza-stream-server/Dockerfile:29`
### LOW-006 — JWT_SECRET hardcoded fallback in docker-compose
**Fichier** : `docker-compose.yml:171``dev-secret-key-minimum-32-characters-long`
---
## INFO FINDINGS
### INFO-001 — Binary database dump dans le repo
**Fichier** : `veza-backend-api/veza_back_api_db/`
### INFO-002 — hash_gen/create_test_user tools use bcrypt.DefaultCost
**Fichiers** : `cmd/tools/hash_gen/main.go:11`, `cmd/tools/create_test_user/main.go:54,72`
### INFO-003 — No JWT key rotation mechanism
**Fichier** : `services/jwt_service.go` — pas de `kid` header, pas de support multi-clés
---
## Contrôles Validés (PASS)
| # | Contrôle | Statut | Preuve |
|---|----------|--------|--------|
| P01 | Injection SQL | ✅ PASS | Toutes les requêtes utilisent des placeholders. ORDER BY protégé par whitelist. |
| P02 | Injection de commande | ✅ PASS | ClamAV et FFmpeg via `exec.CommandContext` avec arguments séparés. |
| P03 | JWT RS256 en production | ✅ PASS | RS256 primary, HS256 dev-only fallback. Algorithm validation empêche `alg: none`. |
| P04 | Secrets non exposés | ✅ PASS | `json:"-"` sur passwords, tokens, secrets MFA, API keys hashées. |
| P05 | crypto/rand (pas math/rand) | ✅ PASS | 19 fichiers utilisent `crypto/rand`. |
| P06 | Webhook HMAC-SHA512 | ✅ PASS | `hmac.Equal()` timing-safe. |
| P07 | Cookie HttpOnly | ✅ PASS | `CookieHttpOnly: true`. |
| P08 | Cookie Secure auto-detect | ✅ PASS | `getCookieSecure()` retourne true en production. |
| P09 | Track metrics hidden (backend) | ✅ PASS | `PlayCount` et `LikeCount` ont `json:"-"` sur le modèle Track. |
| P10 | Rust memory safety | ✅ PASS | Aucun bloc `unsafe`. |
| P11 | GDPR export | ✅ PASS | Endpoint fonctionnel, rate limité (3/24h). |
| P12 | GDPR deletion flow | ✅ PASS | Anonymisation + hard delete 30j. |
| P13 | Password validation | ✅ PASS | Complexité, historique 5 derniers, max 72 bytes. |
| P14 | Account lockout | ✅ PASS | 5 attempts → 30min lockout, Redis + fallback in-memory. |
| P15 | CSRF protection | ✅ PASS | 32-byte tokens, `subtle.ConstantTimeCompare`, Redis storage. |
| P16 | Security headers (backend) | ✅ PASS | HSTS, CSP, X-Frame-Options, COEP/COOP. |
| P17 | Container scanning | ✅ PASS | Trivy scans dans CI/CD. |
| P18 | Secret scanning | ✅ PASS | Gitleaks action. |
| P19 | OAuth PKCE | ✅ PASS | RFC 7636 implémenté (code_verifier + S256 challenge). |
| P20 | OAuth state parameter | ✅ PASS | Généré avec crypto/rand, validé et consommé. |
| P21 | OAuth redirect whitelist | ✅ PASS | URL validée contre whitelist configurée. |
| P22 | Chat room membership | ✅ PASS | `CanRead()`, `CanSend()`, `CanJoin()` vérifient l'appartenance. |
| P23 | Chat XSS protection | ✅ PASS | DOMPurify (frontend) + `html.EscapeString` (backend). |
| P24 | Chat rate limiting | ✅ PASS | Redis sliding window, 10 msg/sec, fallback in-memory. |
---
## Méthodologie
### Agents d'audit spécialisés
6 agents parallèles ont couvert les domaines suivants :
1. **Auth & JWT** — JWT, bcrypt, sessions, cookies, OAuth, MFA, lockout
2. **API IDOR & Access Control** — Authorization bypass, ownership checks, RBAC
3. **Input Validation & Injection** — SQL, XSS, command, path traversal, file upload
4. **Infrastructure & Docker** — Dockerfiles, docker-compose, CI/CD, secrets, CORS
5. **Business Logic & Payments** — Payments, payouts, GDPR, subscriptions, marketplace
6. **WebSocket, Rust & Dependencies** — WS security, memory safety, SCA
### Scope couvert
| Composant | Fichiers analysés |
|-----------|------------------|
| Go backend | ~200 fichiers .go |
| Rust stream server | ~15 fichiers .rs |
| React frontend | ~50 composants clés |
| CI/CD | 11 workflows GitHub Actions |
| Docker | 5 Dockerfiles, 5 docker-compose |
| Migrations | 30+ fichiers SQL |
| Configuration | .env.*, configs/, nginx |
---
*Rapport généré le 2026-03-13 — VEZA v0.12.6 Pentest Security Audit*
*Auditeur : Claude Opus 4.6 + 6 agents spécialisés*