Pourquoi une passerelle à double mode plutôt qu'un rapport de pentest
La plupart des projets du type « j'ai réalisé un audit de sécurité » se résument à une liste statique de vulnérabilités et de scores CVSS — crédible, mais invérifiable pour le lecteur. Ce playground est conçu autrement : une application cible, CyberMart (Express + SQLite, véritable hachage des mots de passe avec bcrypt, authentification JWT), se trouve derrière une Smart Reverse Proxy & Hardening Gateway. La passerelle dispose de deux postures d'exécution, basculées en direct depuis un dashboard de Security Operations :
- VULNERABLE — les chemins de code non assainis de la cible sont exposés directement : SQL construit par concaténation de chaînes, injection DOM brute, authentification par cookie ambiant sans contrôle anti-CSRF, chemins de fichiers relatifs sans protection contre le path traversal.
- HARDENED — exactement le même chemin de requête passe désormais par une inspection WAF au niveau de la passerelle, complétée par des correctifs architecturaux au point d'utilisation des données : requêtes préparées paramétrées, encodage contextuel en entités HTML, confinement des chemins canoniques, jetons CSRF en double soumission et contrôles d'autorisation au niveau objet.
Comme les deux modes exécutent la même application et le même script d'exploitation, aucune démonstration triée sur le volet n'est possible. On envoie le payload d'injection SQL, on le voit extraire la table users en mode VULNERABLE, on bascule la passerelle en HARDENED, on renvoie la même requête à l'octet près, et on obtient 403 Forbidden, avec le nom de la défense qui l'a bloquée dans le corps de la réponse.
La cible : CyberMart et sa surface d'attaque
CyberMart est une application e-commerce modeste mais réaliste — produits, recherche, avis, commandes, factures, parcours de connexion — volontairement construite avec les erreurs que l'on rencontre en audit réel plutôt qu'avec des bugs artificiels de manuel. L'évaluation initiale a relevé huit vulnérabilités distinctes de l'OWASP Top 10 (2021), résumées d'après le rapport de pentest technique :
| Vulnérabilité | OWASP | CVSS | Initial | Durci |
|---|---|---|---|---|
IDOR sur /api/orders/:id | A01 | 8.5 Élevée | 200 OK | 403 |
Secret JWT faible (secret123) | A02 | 7.5 Élevée | 200 OK | 401 |
| Injection SQL de type UNION | A03 | 9.8 Critique | 200 OK | 403 (WAF) / paramétrée au point d'utilisation |
| XSS stockée dans les avis | A03 | 8.0 Élevée | 201 OK | 403 (WAF) / encodée au point d'utilisation |
| Absence de rate limiting sur la connexion | A04 | 5.3 Moyenne | 200 OK | 429 |
| En-têtes de sécurité manquants | A05 | 6.5 Moyenne | Absents | Appliqués (CSP, HSTS, X-Frame-Options…) |
CSRF sur /api/user/email | A07 | 7.4 Élevée | 200 OK | 403 |
| Path traversal sur le téléchargement de factures | A08 | 8.6 Critique | 200 OK | 400 |

Chacune de ces huit vulnérabilités correspond à un correctif précis au point d'utilisation des données, et non à un générique « ajouter de la validation des entrées ». L'objectif de la couche de durcissement n'est pas de bloquer les chaînes d'apparence suspecte — il est de rendre la classe de vulnérabilité structurellement impossible à l'endroit même où la donnée est utilisée.
Étude de cas : la RCE drive-by sur localhost
La vulnérabilité la plus sérieuse de ce projet ne se trouvait pas du tout dans l'application cible — elle se trouvait dans l'outillage conçu pour la démontrer. Le dashboard SOC exposait un endpoint, /api/exploit/run, permettant à un chercheur de déclencher un script d'exploitation depuis l'interface. Deux choix, combinés, le rendaient dangereux : le dashboard utilisait cors() sans aucune restriction d'origine, et le nom de l'exploit était interpolé directement dans une commande shell.
N'importe quel site ouvert dans votre navigateur pouvait lancer calc.exe sur votre machine
Comme le dashboard acceptait les requêtes de toute origine, un site sans aucun rapport — evil-attacker.com, ouvert dans un onglet ordinaire pendant que le lab tournait en local — pouvait émettre en arrière-plan un fetch() silencieux vers http://localhost:3000/api/exploit/run avec un champ exploitType forgé. Le handler construisait sa commande shell sous la forme exec("python security-toolkit/exploits/exploit_" + exploitType + ".py --json") : un payload tel que sqli"; calc.exe # sortait donc de l'argument prévu et exécutait des commandes arbitraires avec les droits de l'utilisateur local. Aucune authentification, aucune interaction au-delà de la simple visite d'une page web — une RCE drive-by classique, logée dans un outil censé enseigner la sécurité web.
La remédiation illustre parfaitement le principe « ne pas assainir la chaîne, supprimer la classe de bug » : le CORS permissif a été remplacé par un contrôle strict de l'origine loopback ; l'interpolation shell a été remplacée par execFile('python', [scriptPath, '--json']), qui transmet les arguments sous forme de tableau immuable sans jamais passer par un interpréteur shell ; enfin, le nom de l'exploit est désormais vérifié contre une liste blanche figée (ALLOWED_EXPLOITS) de huit noms de modules pré-approuvés, de sorte que même une chaîne entièrement contrôlée ne peut atteindre un chemin non prévu.
exec() avec interpolation de chaîne · Correctif : verrouillage de l'origine + execFile avec tableaux d'arguments + liste blanche des noms d'exploitsUne seconde leçon, liée à la première, est venue de la tentative de bloquer l'injection SQL au niveau de la passerelle à l'aide de seules expressions régulières. La règle WAF détectait des motifs comme or '1'='1', et le fuzzing adversarial l'a mise en échec en trois vagues successives : l'asymétrie des guillemets (1' oR '1'='1) a déjoué la correspondance par référence arrière, les frontières de tokens sans espace (zz'or'1'like'1) ont déjoué l'hypothèse \s+, et les mutations d'opérateurs (1'||'1'='1, 1'OR(1)LIKE(1)) ont complètement échappé aux listes de mots-clés. La conclusion documentée dans l'étude de cas est celle que tout éditeur de WAF finit par admettre : le filtrage par regex en périmètre est une couche heuristique perméable, pas un correctif. La véritable remédiation a basculé la requête vers une requête préparée paramétrée native (WHERE name LIKE ?), ce qui rend l'injection mathématiquement impossible quelle que soit la mutation du payload — le WAF reste actif comme signal de télémétrie d'alerte précoce, mais la garantie réelle se situe au point d'utilisation des données.
Vulnérable contre durcie — le même dashboard, deux exécutions
La façon la plus claire d'appréhender la conception à double mode de la passerelle reste le dashboard SOC lui-même, capturé en pleine exploitation dans les deux postures. Chaque paire requête/réponse, chaque code de statut HTTP et chaque libellé de mitigation visibles ci-dessous proviennent de l'exécution réelle de la suite d'exploits contre la passerelle en fonctionnement — il ne s'agit pas d'une maquette.


La vue de la chaîne d'attaque illustre le même point à un niveau plus global. attack_chain_demo.py enchaîne quatre des vulnérabilités individuelles en une compromission continue : un payload XSS stocké dans un avis produit se déclenche lorsque la victime « Alice » consulte la page et envoie silencieusement un POST qui détourne l'adresse e-mail de son compte, faute de contrôle anti-CSRF ; la session détournée sert ensuite à récupérer une commande administrateur confidentielle via IDOR ; la référence de facture de cette commande est enfin injectée dans l'endpoint vulnérable au path traversal pour exfiltrer les identifiants maîtres de la base de données et le secret JWT du serveur. En mode VULNERABLE, les quatre étapes réussissent de bout en bout. En mode HARDENED, la chaîne est rompue à chaque maillon — le WAF et l'encodage en sortie stoppent l'étape un, la vérification du jeton CSRF l'étape deux, le contrôle d'autorisation au niveau objet l'étape trois, et le confinement des chemins canoniques l'étape quatre.

Outillage et détection : automatisation ZAP et véritable couche SIEM
Des scripts d'exploitation manuels prouvent un point une fois ; le scan automatisé et la détection prouvent qu'il tient dans la durée sous des tests continus. Le playground intègre OWASP ZAP de deux manières — un scanner baseline natif et rapide, et le démon ZAP officiel conteneurisé via Docker — ainsi que des workflows documentés Burp Suite Community Edition (Proxy, Repeater, Intruder) pour la vérification manuelle. Les deux chemins ZAP produisent des rapports HTML standard et XML exploitables par machine, de sorte qu'un même scan peut être lu par un humain ou ingéré par un pipeline CI.

Bloquer un exploit connu, ce n'est pas de la détection — c'est de la prévention. Le projet traite ces deux problèmes séparément et construit une petite couche SIEM au-dessus de la télémétrie JSON structurée de la passerelle (gateway/logs/security_events.jsonl). Quatre règles Sigma, écrites en YAML indépendant de tout éditeur, couvrent l'extraction par SQLi UNION, la XSS stockée, le path traversal et l'IDOR, chacune associée à sa technique MITRE ATT&CK. Le problème d'ingénierie intéressant est qu'une vérification de signature sans état ne peut pas distinguer une faute de frappe innocente de la première sonde d'un scanner automatisé — dans les deux cas, on observe une requête isolée. siem_alert_engine.py résout ce problème avec une fenêtre glissante de 30 secondes indexée par IP source, qui suit la vitesse de mutation et la diversité structurelle des payloads (styles de guillemets, délimiteurs de commentaires, endpoints ciblés). Lorsque cinq mutations distinctes ou plus atteignent plusieurs endpoints depuis une même IP dans cette fenêtre, le bruit des événements isolés est supprimé et une unique alerte composite SCANNING_CAMPAIGN_DETECTED est levée, associée à MITRE ATT&CK T1595.002 — toute la différence entre « voici une ligne de log » et « voici un incident ».


Prouver le correctif plutôt que l'affirmer
Une affirmation de durcissement ne vaut que par les tests de non-régression qui la soutiennent. La suite comprend 18 tests Jest unitaires et d'intégration couvrant le hachage des mots de passe avec bcrypt, l'isolation des jetons et l'échappement contextuel, ainsi qu'un fuzzer par mutation fondé sur les propriétés qui soumet au WAF 2 040 payloads structurellement variés — styles de guillemets, motifs d'espacement, encodages et combinaisons d'opérateurs différents — afin d'éprouver précisément la classe de contournement qui avait mis en échec la première version du WAF à base de regex lors de l'audit de la RCE drive-by. Chaque push déclenche également un pipeline GitHub Actions de 21 étapes qui scanne spécifiquement la posture durcie : si une régression réintroduite laisse passer le moindre payload en mode HARDENED, le build échoue purement et simplement au lieu de passer silencieusement grâce à `|| true`.


Choix d'ingénierie qui méritent qu'on s'y attarde
- Isolation hiérarchisée des clés JWT — une première version durcie vérifiait indistinctement chaque jeton contre un unique secret d'entreprise, ce qui bloquait les utilisateurs légitimes dont les jetons étaient signés avec la clé historique de l'application. Le correctif sépare la vérification selon le privilège revendiqué : les sessions standard utilisent les clés standard, les revendications administrateur sont vérifiées contre un secret distinct à forte entropie, de sorte qu'un jeton forgé après cassage par dictionnaire (signé avec le
secret123divulgué) est rejeté cryptographiquement tandis que les vrais utilisateurs continuent de travailler. - Encoder au point d'utilisation, pas au niveau de la passerelle — un premier correctif XSS échappait deux fois le texte des avis, à l'écriture et à la lecture, transformant des apostrophes légitimes en
&illisible. La conception finale stocke les commentaires bruts et applique un encodage en entités HTML sensible au contexte une seule fois, au point de sortie de présentation, conformément au modèle canonique de l'OWASP. - Stockage des mots de passe migré d'une comparaison de chaînes en clair vers bcrypt salé (`bcrypt.compare`), ce qui supprime entièrement l'exposition des identifiants en clair au repos, plutôt que d'ajouter une étape de hachage par-dessus une logique de comparaison défaillante.
- La barrière CI bloque réellement — elle est testée en réintroduisant délibérément une régression SQLi connue et en vérifiant que le pipeline échoue, plutôt qu'en supposant que le scanner l'aurait détectée.
La documentation technique complète — le modèle de menaces et les diagrammes de flux de données STRIDE, les diffs de code avant/après pour les huit vulnérabilités, les guides d'automatisation ZAP et d'utilisation de Burp Suite, ainsi qu'un modèle de rapport de bug bounty — se trouve dans les dossiers docs/ et reports/ du dépôt.