Audit de sécurité de la vitrine de démonstration — Round 9
TL;DR
- 6 findings ouverts, 6 corrigés et re-validés en un cycle. Dont une découverte faite pendant le re-test (chaîne de prise de compte sur la récupération assistée), escaladée puis fermée en profondeur.
- Ce qui n'est jamais tombé : le mémo chiffré de bout en bout (clé jamais côté serveur), le contrôle d'accès du forum (IDOR / mass-assignment), l'injection SQL, l'élévation de privilèges (racine du serveur non conquise).
- Durcissement réseau prouvé en conditions réelles : après correctif, une rafale d'attaque depuis un poste interne est bannie au niveau pare-feu en quelques secondes.
1. Périmètre et méthode
Cible : un banc de démonstration en réseau local (Linux Debian 13), hébergeant les modules MySelf sous forme de sites séparés :
| Module | Rôle |
|---|---|
| Landing | page de présentation |
| SelfRecover | récupération de compte sans e-mail (niveaux L1 / L2 / L3 + variante Lite) |
| SelfDataGuard | chiffrement des données |
| Forum (Lab) | auth SelfRecover, messages privés, mémo chiffré de bout en bout, modération |
| SelfJustice | directives juridiques |
Accès : boîte grise raisonnable — un compte système non privilégié (la racine restait à conquérir), lecture de configuration autorisée, comptes applicatifs créés librement via l'inscription ouverte.
Outils (versionnés) : nmap, ssh-audit, sslscan, nuclei, httpx, ffuf, curl, scripts Python, plus des tests manuels (IDOR, mass-assignment, logique métier, cas-limites de modération, défi du mémo chiffré). Deux passes : audit applicatif depuis un poste de confiance, puis vérification que les défenses réseau bloquent réellement un poste non listé.
2. Ce que la vitrine protège
Défenses éprouvées qui tiennent.
| Défense | Vérification | Statut |
|---|---|---|
| Mémo chiffré de bout en bout | Aucune voie HTTP ne sort le mémo en clair : la clé est dérivée du mot de passe dans le navigateur, jamais transmise. | ✅ imprenable |
| Messages privés (chiffrés au repos) | Clé hors racine web, non lisible sans compromission disque (non atteinte). | ✅ tient |
| Contrôle d'accès forum (IDOR) | L'identifiant de ressource provient toujours de la session ; les paramètres injectés (account_id, id…) sont ignorés. | ✅ tient |
| Mass-assignment | Champ privilégié injecté dans une mise à jour de profil → ignoré, aucune élévation. | ✅ tient |
| Injection SQL | Requêtes préparées partout ; scanners et tests manuels négatifs. | ✅ tient |
| Modération coordonnée (pack-voting) | 3 votes négatifs coordonnés → neutralisés en temps réel, réputation restaurée, aucun bannissement injuste, vote légitime conservé. | ✅ tient |
| Exposition de fichiers | Aucune fuite : les accès à .git, .env, base, sauvegardes renvoient la page d'accueil (contrôleur frontal), vérifié par le contenu. | ✅ tient |
| Surface réseau | Seuls 3 ports publics ; bases de données en écoute locale uniquement. | ✅ tient |
| SSH | Version récente, aucun algorithme faible, authentification par clé. | ✅ tient |
| Élévation de privilèges | Aucun chemin vers la racine : mot de passe requis, sudoers illisible, aucun secret world-readable. Racine non conquise. | ✅ tient |
| En-têtes HTTP | HSTS, CSP, anti-clickjacking, anti-sniffing sur tous les modules ; bannière serveur masquée. | ✅ tient |
Le cœur cryptographique de MySelf résiste à un attaquant authentifié disposant du code source.
3. Ce que nous avons trouvé — et corrigé
Vue d'ensemble. Chaque ligne a été re-testée après remédiation.
| # | Sévérité | Finding | Re-test |
|---|---|---|---|
| R9-01a | incluse §4 | Énumération séquentielle des dossiers de récupération | ✅ fermé |
| R9-01b | ÉLEVÉE | Chaîne de prise de compte via la récupération assistée (voir §4) | ✅ fermé |
| R9-02 | Moyenne | Exposition publique du sel de dérivation des clés | ✅ fermé |
| R9-03 | Moyenne | Durcissement PHP incomplet (fonctions dangereuses non désactivées) | ✅ fermé |
| R9-04 | Moyenne | Défenses réseau aveugles aux sources internes (voir §5) | ✅ fermé |
| R9-05 | Faible | Jeton d'administration par défaut codé en dur (repli) | ✅ fermé |
| R9-06 | Info | Algorithme de hachage non uniforme entre modules | ✅ fermé |
Correctifs en bref :
- R9-02 : suppression de l'endpoint exposant le sel ; passage à un sel par-utilisateur généré côté client.
- R9-03 : désactivation des fonctions d'exécution/processus via un fichier de durcissement dédié.
- R9-05 : jeton d'administration via variable d'environnement uniquement, sans repli — échec fermé si absent.
- R9-06 : hachage uniformisé (Argon2id) sur tous les modules, avec ré-encodage transparent des anciens hachages à la connexion.
4. Étude de cas — la chaîne de prise de compte (R9-01)
C'est le cas le plus instructif, car il illustre un correctif partiel qui révèle un problème plus profond, puis sa fermeture en couches.
4.1 Le finding initial
La consultation d'un dossier de récupération de niveau 3 se faisait sans autorisation : le numéro de dossier était séquentiel et énumérable. Un visiteur anonyme pouvait parcourir les dossiers et lire les conversations de preuve d'identité. (Un marqueur déposé dans une conversation a été exfiltré en une requête.)
4.2 Le premier correctif — et son angle mort
La correction a rendu le numéro non devinable (jeton aléatoire) + anti-bruteforce. L'énumération était bien morte. Mais le re-test a montré que ça ne suffisait pas :
- L'ouverture d'un dossier n'exigeait aucune authentification : avec le seul identifiant (semi-public) d'une cible, on récupérait le numéro de dossier déjà ouvert.
- La lecture/écriture de la conversation n'était liée à aucune session : tout porteur du numéro lisait et écrivait.
- À l'accord de l'administrateur, le nouveau mot de passe était inscrit dans cette conversation.
→ Enchaînés : identifiant connu → numéro → lecture de la conversation → récupération du mot de passe régénéré dès l'accord = prise de compte. Le finding a été réévalué à la hausse (ÉLEVÉE).
4.3 Le correctif en profondeur (re-validé)
Quatre verrous, sans casser la contrainte structurelle « en récupération assistée, le propriétaire n'a plus de session » :
- Sésame du demandeur : à l'ouverture, le client génère un secret aléatoire et n'en envoie que l'empreinte. Si un dossier existe déjà, le numéro n'est plus redonné à un nouvel appelant — et la tentative concurrente devient un signal de détection pour l'administrateur.
- Conversation autorisée par le sésame (et en POST uniquement : plus de numéro dans une URL journalisable). Sans sésame valide → accès refusé.
- Plus aucun secret dans un canal lisible : l'accord ne génère ni ne transmet de mot de passe. Le propriétaire re-définit lui-même son secret (modèle de ré-enrôlement, cohérent avec la philosophie MySelf : le serveur ne voit jamais le mot de passe, même en récupération).
- Expiration + capability à usage unique : le dossier expire, et le sésame est consommé au reset.
Re-test après correctif — chaîne rejouée de bout en bout :
| Étape de l'attaque | Résultat |
|---|---|
| Ouvrir un dossier avec l'identifiant d'autrui | « déjà ouvert », aucun numéro divulgué |
| Lire la conversation sans le sésame | refusé (403) |
| Lire via une URL (query-string) | neutralisé (POST uniquement) |
| Re-définir le secret sans accord administrateur | refusé |
| Rejouer la chaîne avec le seul identifiant | aucun accès, aucun secret |
✅ Chaîne brisée à chaque maillon. Le propriétaire légitime, lui, conserve son parcours.
5. Étude de cas — défenses réseau et angle mort interne (R9-04)
Le banc utilise un IDS comportemental + pare-feu. Le finding : par défaut, l'IDS exemptait les plages d'adresses privées — autrement dit, aucune protection contre un poste interne malveillant (mouvement latéral). Le pipeline fonctionnait (100 % des journaux analysés), mais toutes les requêtes d'attaque internes étaient « whitelistées ».
Correctif : retrait de l'exemption globale des plages privées, remplacée par une liste explicite de postes de confiance.
Re-test après correctif (preuve en conditions réelles) :
État nominal : HTTP 200 · SSH OK
Rafale d'attaque : depuis un poste interne non listé
→ premier blocage : ~6 secondes
État final : HTTP en timeout · SSH coupé · ICMP sans réponse
(l'adresse entière est droppée au niveau pare-feu)
✅ Un attaquant interne est désormais détecté et bloqué.
6. Limites assumées (honnêteté)
- Décision humaine en récupération de niveau 3 : la validation finale d'identité reste une décision d'administrateur. Le correctif rend la chaîne technique saine ; la diligence de l'administrateur demeure le dernier maillon — par conception. Une tentative concurrente déclenche désormais une alerte « multi-demandeur ».
- Audit assisté par IA, pas red team externe : ce cycle est notre préparation défensive. C'est précisément pour ça que nous ouvrons le banc à des équipes externes.
7. Invitation aux équipes de sécurité
Ce cycle montre notre posture : surface minimale, cœur cryptographique qui tient, et un processus trouver → corriger → re-valider documenté et daté. Les marqueurs « à défendre » (mémo de bout en bout, secrets de récupération, racine du serveur) n'ont pas été sortis.
Si vous êtes une équipe red team et souhaitez éprouver MySelf en conditions réelles, c'est exactement ce que nous recherchons. Les éléments protégés ci-dessus sont des défis ouverts.