Move ASVS_CHECKLIST_v0.12.6.md, PENTEST_REPORT_VEZA_v0.12.6.md, and REMEDIATION_MATRIX_v0.12.6.md to docs/archive/ — all reference a pentest conducted on v0.12.6 (2026-03), stale relative to the current v1.0.7 codebase (different security middleware, different payment flow, different config validation). Update CLAUDE.md tree listing and AUDIT_REPORT.md §9.1 to reflect the archive location. Keep docs/SECURITY_SCAN_RC1.md (still current). Closes AUDIT_REPORT §9.1 obsolete-doc item. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
397 lines
16 KiB
Markdown
397 lines
16 KiB
Markdown
# 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*
|