# Audit de sécurité E2EE leggit — v1.0

**Référence** : ATT-E2EE-2026-05-16-v1.0
**Date** : 2026-05-16
**Périmètre** : leggit/coffre (app.leggit.org) — pile E2EE complète après ajout du mode Prudent dual-device, table de substitution, et refactor relations
**Auditeur** : Agent security-engineer (Claude) avec brief explicite focalisé sur les changements récents
**Statut** : ARCHIVÉ — remplacé par v2.0 du 2026-05-18

---

## Synthèse

| Sévérité | Nombre | Statut |
|---|---|---|
| CRITICAL | 0 | — |
| **HIGH** | **1** | ✅ Corrigé |
| MEDIUM | 4 | ✅ Corrigés |
| LOW | 4 | ✅ Corrigés |
| INFO | 3 | Documentés |
| **Total** | **12** | **9 fixés, 3 info** |

**Verdict** : Production avec correctifs appliqués. Aucune faille cryptographique. Findings principalement des bypasses de rate-limit et vecteurs d'énumération.

---

## Findings détaillés

### [HIGH-1] Énumération d'utilisateurs par téléphone via `tiers.actionInvite` + `coffres.actionAddRecipient`

**Fichiers** :
- `modules/tiers/api.php:79-104` (actionInvite)
- `modules/coffres/api.php:1812-1838` (actionAddRecipient)

**Description** : Les actions acceptent `telephone_lookup`. Si email vide, le serveur résout l'email via `SELECT mail FROM utilisateurs WHERE telephone = :p`. La différence de réponse entre numéro trouvé/non trouvé constitue un oracle d'énumération permettant à tout user authentifié de tester N numéros et savoir lesquels sont sur leggit. Bonus aggravant : un numéro reconnu déclenche un email d'invitation à la victime (spam).

**Fix appliqué** :
- Création de `core/rate_limit.php` (helper file-based générique)
- Rate-limit user+IP : 20 invitations/heure dans `actionInvite` et `actionAddRecipient`
- Message d'erreur générique aligné entre tous les cas ("Identification invalide (email ou telephone manquant ou inconnu).")
- Audit log `tier.invite_rate_limited` / `coffre.add_recipient_rate_limited`

---

### [MEDIUM-1] Rate-limit `pairing_resolve` session-based (contournable)

**Fichier** : `modules/crypto-wallets/api.php:798-812`

**Description** : Le compteur était stocké dans `$_SESSION` → un attaquant pouvait logout/relancer un incognito pour réinitialiser le compteur.

**Fix appliqué** : Migration vers `leggitRateLimit()` file-based (clé user+IP, 10 tentatives/15min, indépendant de la session/cookie).

---

### [MEDIUM-2] `actionCompleteTable` sans rate-limit

**Fichier** : `modules/crypto-wallets/api.php:887` (actionCompleteTable)

**Description** : `pairing_resolve` avait un rate-limit mais `complete_table` n'en avait aucun, alors qu'elle accepte aussi le code court 6 chars → bypass évident.

**Fix appliqué** : Même rate-limit file-based avec clé `pairing_complete:user:ip` (10 tentatives/15min).

---

### [MEDIUM-3] CRLF injection dans `desired_filename`

**Fichiers** :
- `modules/coffres/api.php:1030-1052` (actionUploadFile legacy)
- `modules/coffres/api.php:1408-1419` (actionUploadFinalizeE2EE)

**Description** : Le filtre `[/\\:*?"<>|]` ne retirait ni les caractères de contrôle (`\r`, `\n`, NULL) ni `..`. Risque : self-DoS sur l'header `Content-Disposition`, pollution des audit logs.

**Fix appliqué** :
```php
$desiredName = preg_replace('/[\x00-\x1F\x7F]/u', '', $desiredName);
$desiredName = preg_replace('/[\/\\\\:*?"<>|]/', '', $desiredName);
$desiredName = str_replace('..', '_', $desiredName);
```
Appliqué identiquement sur les 2 endpoints.

---

### [MEDIUM-4] `cryptoWalletPairingCleanup` jamais appelé

**Fichier** : `core/crypto_wallets.php:1187-1240`

**Description** : La fonction de nettoyage des pairings expirés existait mais aucun cron ne l'appelait → la table `crypto_wallets_pairing` grossissait sans borne.

**Fix appliqué** :
- `cron/cleanup_worker.php:372-381` appelle désormais `cryptoWalletPairingCleanup()` quotidiennement
- Bonus : quota max 10 wallets `en_attente_table` par user (audit `crypto_wallet.defer_quota_exceeded`)

---

### [LOW-1] `Cache-Control: no-store` sur `complete.php`

**Fichier** : `modules/crypto-wallets/complete.php:30-31`

**Description** : La page reçoit le `pairing_token` long dans l'URL → risque de persistance dans caches proxies/CDN/navigateur.

**Fix appliqué** : `Cache-Control: no-store, no-cache, must-revalidate, private` + `Pragma: no-cache` en début de page.

---

### [LOW-2] Audit `pairing_resolved` sans `id_coffre`

**Fichier** : `modules/crypto-wallets/api.php:863-886`

**Description** : L'appel `auditCrypto($idUser, null, 'crypto_wallet.pairing_resolved', ...)` passait `null` pour `id_coffre` alors que l'info était disponible.

**Fix appliqué** : `cryptoWalletPairingResolve()` retourne maintenant `id_coffre` ; passé à `auditCrypto`.

---

### [LOW-3] Reveal "Table seule" : seed déchiffrée inutilement

**Fichier** : `assets/js/crypto-wallets-module.js`

**Description** : Quand l'utilisateur choisissait "Table seule" dans le chooser, la seed était quand même déchiffrée en RAM JS (faux sentiment de sécurité dual-device).

**Fix appliqué** :
- Pre-chooser `askDualDeviceRevealChoice()` AVANT le déchiffrement
- Nouvelle fonction `openTableOnlyRevealModal()` qui n'appelle PAS `decryptItem`
- Branche "Table seule" → return avant `LeggitCoffreCrypto.decryptItem`

---

### [LOW-4] Referrer-Policy global (déjà en place)

**Fichier** : `bootstrap.php:56`

**Description** : Le token long dans l'URL pourrait fuiter via Referer.

**Statut** : `Referrer-Policy: no-referrer` était déjà posé globalement. Test E2E ajouté pour bloquer toute régression future.

---

## INFO (recommandations non-bloquantes)

- **INFO-1** : Migration 049 (unique téléphone) — la dédup auto en cas de conflit BDD pourrait priver un user légitime de son téléphone. Communication user post-migration recommandée.
- **INFO-2** : BaconQrCode (lib QR-code) — dépendance composer à inclure dans la veille sécurité régulière.
- **INFO-3** : `stopPropagation` dans le reauth handler — vérifié sans risque (pas de listener global de sécurité bloqué).

---

## Files de test

- `tools/test-e2e-phase11.php` sections **P11.52 à P11.60** (~25 assertions défensives anti-régression)
- Tous PASS au moment de l'audit (753+ assertions globales)

## Conclusion v1.0

Tous les findings HIGH et MEDIUM exploitables ont été corrigés. La crypto E2EE elle-même (XChaCha20-Poly1305, X25519, Argon2id, Shamir) n'a été ni dégradée ni modifiée. Le verdict bascule en **production-ready** après application des correctifs.

Cette version d'audit est archivée comme référence historique. Voir `2026-05-18-audit-e2ee-v2.0.md` pour l'audit suivant qui vérifie la non-régression de ces fixes + audite les changements postérieurs.
