Des bots ont mis à genoux un Magento 2 en spammant une simple page d’inscription — sans créer un seul compte. Retour d’expérience : pourquoi une page non cacheable suffit à saturer un serveur, et le scénario CrowdSec maison écrit pour stopper l’attaque.
Le contexte : un site déjà dans le viseur
Le client (e-commerce, que je ne nommerai pas) avait un historique chargé : sa boutique PrestaShop s’était fait hacker quelques années plus tôt. C’est d’ailleurs ce qui l’avait amené chez nous pour une refonte complète sous Magento 2. Mais un site qui a été compromis une fois reste durablement dans les listes de cibles : les bots revenaient frapper en permanence, à l’affût de la moindre faiblesse.
La stack était saine — Magento 2 à jour, serveur durci, et CrowdSec déjà en place avec la collection Apache du Hub. Les scénarios classiques (bruteforce SSH, scan d’URL, user-agents malveillants) faisaient leur travail. Et pourtant.
Le symptôme : un serveur qui tombe, des logs « propres »
Par vagues, le serveur tombait complètement : Apache et PHP-FPM saturés, le site inaccessible. Premier réflexe : chercher l’attaque dans ce que CrowdSec connaît — tentatives de login, scans, 404 en rafale. Rien. Les requêtes en cause étaient des GET en 200, parfaitement légitimes en apparence.
En dépouillant les access logs, le motif a fini par sauter aux yeux : des centaines de GET, depuis une multitude d’IP, toutes sur la même URL :
GET /customer/account/create/ HTTP/1.1" 200
La page de création de compte. Pas de soumission de formulaire, pas de faux comptes : juste l’affichage de la page, en masse. On pense d’abord à du spam d’inscription — les logs racontaient autre chose.
Pourquoi cette page fait si mal
Sur Magento 2, /customer/account/create/ est une page non cacheable : elle démarre une session client et génère un form_key unique à chaque visite. Le full page cache (Varnish ou FPC) la laisse donc systématiquement passer. Conséquence : chaque hit tape PHP et MySQL en direct, sans aucun amortisseur.
Pour l’attaquant, c’est l’attaque par épuisement de ressources la moins chère du monde : des GET anodins, en 200, qui passent sous tous les radars classiques — et un backend qui s’écroule. Aucune vulnérabilité exploitée, aucune trace « sale » dans les logs.
Le scénario CrowdSec maison
La bonne nouvelle : le parser Apache de la collection crowdsecurity/apache2 structurait déjà tout ce qu’il me fallait (evt.Parsed.request, evt.Meta.http_status, evt.Meta.source_ip). Pas besoin d’écrire un parser — juste un scénario par-dessus :
type: leaky
name: antiseptikk/magento-create-page-flood
description: "Detect GET flood on Magento 2 account creation page"
filter: "evt.Meta.log_type == 'http_access-log' && evt.Parsed.verb == 'GET' && evt.Parsed.request == '/customer/account/create/' && evt.Meta.http_status == '200'"
groupby: evt.Meta.source_ip
capacity: 2
reprocess: true
leakspeed: "10s"
blackhole: 1m
labels:
service: http
type: dos
remediation: true
Ligne par ligne :
type: leaky: un leaky bucket — le seau se remplit à chaque requête qui matche, et fuit à vitesse constante. Il déborde ? L’IP est signalée.filter: uniquement les GET en 200 sur la page de création de compte. Le verbe explicite documente l’intention (on vise le flood d’affichage, pas la soumission du formulaire), et le200écarte au passage les redirections des clients déjà connectés.groupby: evt.Meta.source_ip: un seau par IP.capacity: 2+leakspeed: 10s: plus de 3 affichages de la page en ~20 secondes déclenche le ban. Agressif ? Assumé : aucun humain ne recharge la page d’inscription trois fois en vingt secondes.blackhole: 1m: une fois l’alerte levée, on n’en regénère pas pour la même IP pendant une minute (la décision de ban, elle, court bien plus longtemps).
Un scénario local se dépose simplement dans /etc/crowdsec/scenarios/, suivi d’un reload. Pour vérifier qu’il matche bien avant de le lâcher en prod, cscli explain rejoue de vrais logs dans la chaîne de parsing et montre, étape par étape, ce qui matche ou pas :
cscli explain --file access.log --type apache2
Le résultat
Je n’ai malheureusement plus les métriques d’époque (le client est parti depuis, les dashboards avec lui), donc pas de beau graphique avant/après — je vous dois l’honnêteté sur ce point. Mais le constat opérationnel était sans ambiguïté : avant, le serveur tombait complètement à chaque vague ; après, plus aucune interruption. Les vagues continuaient d’arriver, les IP se faisaient bannir au bout de quelques secondes, et le backend ne voyait plus passer le flood.
Ce que j’en retiens
- Toute page non cacheable est une surface d’attaque : création de compte, login, panier, recherche… Sur un Magento (ou n’importe quel framework), ce sont elles qu’il faut surveiller — pas seulement les 404 et les tentatives de login.
- Le bon pattern CrowdSec : parsers du Hub + scénarios maison. Les collections officielles fournissent les champs structurés ; la connaissance métier de votre application, c’est vous qui l’avez. Écrire un scénario, c’est 15 lignes de YAML.
- Lire les logs avant de conclure. Je pensais spam de création de comptes ; c’était un flood d’affichage. La parade n’est pas la même.
- Nommer juste compte aussi. Mon premier jet s’appelait
magento-frontend-bf, labelbruteforce— un mauvais nom : aucun mot de passe n’était attaqué, c’était un flood. Je l’ai renommémagento-create-page-floodavec un labeltype: dos. Au-delà de l’hygiène, ces labels pilotent la qualification des alertes et les remédiations côté console CrowdSec.