PrestaShop sécurité : bloquer la navigation à facettes via fail2ban

Guide pratique pour utiliser fail2ban afin de protéger PrestaShop contre les crawls combinatoires et l’abus de l’endpoint ps_facetedsearch, sans nuire au SEO.

Écrans présentant des schémas de sécurité pour l'architecture e-commerce.

Table des matières :

  1. Ce que la navigation à facettes coûte réellement côté serveur PrestaShop
  2. Modèles d’abus : crawl combinatoire, cache‑busting et pseudo‑DDoS couche 7
  3. Pourquoi fail2ban est pertinent (et ce qu’il ne fera pas pour vous)
  4. Implémentation fail2ban : filtrer ps_facetedsearch et les URLs avec q=
  5. Réduction des faux positifs : reverse proxy, IPv6, bots légitimes, et seuils réalistes
  6. Mesurer l’impact et garder une posture SEO propre (sans casser l’indexation)
  7. Checklist de déploiement (production) pour éviter les erreurs classiques

Ce que la navigation à facettes coûte réellement côté serveur PrestaShop

La « navigation à facettes » dans PrestaShop (module ps_facetedsearch, ex‑« layered navigation ») n’est pas un simple filtrage front. C’est une chaîne de requêtes PHP + SQL déclenchée à chaque combinaison de filtres, souvent via un appel AJAX. Sur PrestaShop 8.1 / 9.x (Symfony 6.4 côté BO, FO majoritairement legacy), ce module calcule dynamiquement les facettes disponibles (comptage par attribut, prix, marques, etc.) puis renvoie un HTML/JSON à injecter dans la page. Le problème : à l’échelle d’un catalogue “réaliste” (10k–200k produits), la plupart des combinaisons “cassent” les caches applicatifs et sollicitent MySQL de manière non linéaire.

Ce caractère non linéaire vient surtout de deux choses :

  • Explosion de l’espace d’URL : même si chaque facette prise isolément semble “raisonnable”, les combinaisons augmentent vite. Une approximation utile pour se faire un ordre de grandeur est :
    N ≈ (v1 + 1) × (v2 + 1) × ... × (vk + 1) − 1 (chaque facette est “non choisie” ou choisie parmi vi valeurs).
    Exemple très concret sur une catégorie “mode” (fréquent en e‑commerce FR) : 6 facettes avec en moyenne 8 valeurs chacune → (8+1)^6 − 1 = 531 440 variantes possibles sur une seule catégorie, avant même d’ajouter tri/pagination.
  • Coût “listing produit” : le listing n’est pas un SELECT simple. Il combine filtres, jointures, comptages, parfois des calculs de prix, et un ORDER BY (prix, nouveauté, popularité…) qui peut forcer des traitements coûteux si la requête n’est pas alignée avec des index adaptés.

Concrètement, on voit deux familles d’URL en production :

  • Pages catégories/collections avec paramètres de facettes, typiquement ?q=Couleur-Rouge/Taille-M ou ?q=Marque-XXX&order=product.price.asc (selon thème et configuration des URLs).
  • Endpoint du module (souvent /module/ps_facetedsearch/search), utilisé pour recalculer les facettes et la liste produits en mode AJAX.

Le point souvent sous‑estimé : la query string change la clé de cache dans la majorité des setups. Même avec un reverse proxy, un CDN ou un cache HTTP, les URLs facettées sont rarement “cache‑friendly” par défaut (variantes illimitées, contenu dépendant de stocks/prix, cookies, etc.). Résultat : vous payez le coût PHP + DB à répétition.

Si vous avez déjà fait un audit perf sérieux, vous savez que les requêtes “listing produit” sont parmi les plus coûteuses : elles combinent jointures, tris, paginations, et parfois des sous‑requêtes pour les prix spécifiques. Quand un bot génère 1 000 variantes de q= sur une catégorie, vous obtenez un DoS applicatif “propre” : CPU PHP‑FPM qui grimpe, saturation du pool de connexions, et MySQL qui passe son temps à exécuter des requêtes proches mais jamais identiques (donc peu amorties par le cache de requêtes côté applicatif, et rarement compatibles avec un cache HTTP).

Pour remettre ça à plat côté perf, gardez sous la main des repères comme ceux de Performance PrestaShop : benchmarks et optimisation PHP‑FPM, OPCache, MySQL et du runbook TTFB PrestaShop : réduire le Time To First Byte sous 200 ms.

Modèles d’abus : crawl combinatoire, cache‑busting et pseudo‑DDoS couche 7

Le pattern le plus courant est le crawl combinatoire : un bot (scraper, concurrent, SEO “mal configuré”, ou scanner opportuniste) itère sur toutes les valeurs d’attributs d’une catégorie, puis sur toutes les paires, triples, etc. Sur un catalogue avec 12 facettes actives et 5–50 valeurs chacune, le nombre de combinaisons explose. Même si le bot ne visite qu’une fraction de l’espace de recherche, il suffit de quelques centaines de requêtes/minute pour dégrader une infra “VPS standard” : le coût n’est pas la bande passante, c’est le temps CPU + DB par requête.

Dans les logs, ce crawl combinatoire se repère généralement par un mix de signaux (aucun n’est parfait seul) :

  • Rythme et régularité : séries longues et régulières (ex. 1 requête toutes les 200–400 ms) sur des URLs de facettes.
  • Entropie des URLs : beaucoup de q= différents sur un court laps de temps, parfois avec des ordres de tri/pagination peu “humains”.
  • User‑Agent : soit vide, soit “browser‑like” mais incohérent (pas d’Accept-Language, pas de Referer, ou un UA qui change trop souvent).
  • Statuts HTTP : paradoxalement beaucoup de 200 (car le module répond), ce qui rend l’abus plus dangereux que des scans 404 faciles à filtrer.

Deuxième pattern : le cache‑busting via paramètres “inutiles” ou ordres/tri/pagination. Une URL du type /12-chemises?q=Couleur-Rouge&order=product.price.asc&resultsPerPage=96&page=17 est une anti‑thèse du cache HTTP : variation constante de la clé, génération de contenu non réutilisable, et surcharge des index MySQL (surtout si le tri n’est pas couvert par les bons index). Si vous voulez visualiser les symptômes : augmentation du 95e percentile de latence sur les pages catégories, montée de Threads_running, hausse du ratio de “slow queries”, et multiplication des SELECT à fort coût.

Sur ce volet, l’article Index MySQL : optimiser WHERE, JOIN et ORDER BY avec EXPLAIN aide à objectiver : si votre ORDER BY force un filesort et que vos filtres changent à chaque requête, un bot “gentil” peut suffire à vous mettre en difficulté.

Troisième pattern : l’attaque “ciblée” sur l’endpoint AJAX ps_facetedsearch. C’est la cible parfaite : elle est stable (donc facile à automatiser), elle déclenche un recalcul, et elle est rarement protégée par un WAF sur des petites/moyennes boutiques. Ce n’est pas du volumétrique façon DDoS L3/L4 ; c’est du L7 “à petit débit” mais à coût serveur élevé.

Un point pratique : ce type d’abus arrive souvent lors de périodes de forte charge business (soldes, promos, campagnes Shopping/Meta), car la boutique est déjà “au bord” et les listings sont davantage sollicités. Dans ce contexte, réduire les hits inutiles sur les facettes peut être un levier de stabilisation rapide, en attendant les corrections structurelles (caching, SEO, indexes, règles de crawl).

Pourquoi fail2ban est pertinent (et ce qu’il ne fera pas pour vous)

fail2ban est un outil de sécurité réactif : il lit des logs, matche des motifs, et déclenche des bans temporaires (iptables/nftables, firewalld, etc.). Selon la description du projet, il analyse des fichiers de logs et bannit les IPs dont le comportement ressemble à des usages malveillants (projet Fail2ban). Autrement dit : ce n’est pas un WAF, ce n’est pas un rate limiter fin, et ce n’est pas une protection CDN.

Son intérêt sur la navigation à facettes est pragmatique : quand vous identifiez une signature d’abus dans vos logs (trop de hits sur /module/ps_facetedsearch/search, trop d’URLs avec q= en rafale, etc.), vous pouvez couper net l’IP sans toucher au code PrestaShop ni au thème. C’est particulièrement utile sur des environnements où vous ne pouvez pas déployer rapidement un WAF (ou quand la partie réseau est hors de portée, typiquement mutualisé/VPS managé minimal).

Ce que fail2ban fait bien dans ce cas d’usage :

  • Stopper vite un abus “mono‑IP” (ou un petit ensemble d’IPs) qui coûte cher en CPU/DB.
  • Servir de filet de sécurité pendant que vous corrigez les causes (SEO, limitations HTTP, cache, index).
  • Appliquer des bans temporaires (plutôt que des blocages définitifs) — important pour réduire le risque de bloquer un client réel.

Ses limites sont structurelles. D’abord, il bannit une IP : si vous êtes derrière un CDN / reverse proxy et que vos logs ne contiennent pas la vraie IP client, vous allez bannir… l’IP du proxy (donc auto‑sabotage). Ensuite, comme il s’appuie sur des logs, il réagit “après” quelques requêtes. Pour du rate limiting proactif, Nginx/Apache font mieux : Nginx fournit par exemple un module dédié à la limitation du débit de requêtes par clé (documentation officielle : ngx_http_limit_req_module).

Dans l’idéal : rate limiting au niveau HTTP + fail2ban en backstop + éventuellement WAF (cf. Sécurité PrestaShop : protéger API backoffice, WAF et journalisation SIEM).

Implémentation fail2ban : filtrer ps_facetedsearch et les URLs avec q=

Contexte technique (à adapter, sinon vous allez vous tirer une balle dans le pied) : exemples validés pour Debian 12 / Ubuntu 22.04–24.04 avec fail2ban 1.0+, Nginx ou Apache, PrestaShop 8.1.x et 9.0–9.1, PHP 8.2/8.3. Prérequis : accès root, logs d’accès HTTP activés, et une stratégie claire d’allowlist (IP bureau, IP supervision, etc.). Risque principal : faux positifs (ban d’un client réel ou d’un outil interne), et ban du proxy/CDN si la “real IP” n’est pas correctement loggée.

Avant d’écrire une règle, commencez par confirmer la signature dans vos logs. Exemples rapides (Nginx) :

# Top IPs sur l'endpoint AJAX du module
awk '($7 ~ /\/module\/ps_facetedsearch\/search/){print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -nr | head

# Top IPs sur les URLs contenant q=
grep -E '"(GET|POST) [^ ]*\?[^ ]*q=' /var/log/nginx/access.log \
  | awk '{print $1}' | sort | uniq -c | sort -nr | head

Astuce utile pour qualifier “bot vs humain” sans outil lourd : calculez un débit par IP sur une fenêtre courte.

# Débit brut sur 60s (approx) : compte par IP sur les 2000 dernières lignes
tail -n 2000 /var/log/nginx/access.log \
  | awk '{print $1}' | sort | uniq -c | sort -nr | head

Ensuite, créez un filtre fail2ban dédié. Exemple (Nginx combined ou main où $request contient la query string). Fichier : /etc/fail2ban/filter.d/prestashop-facets.conf

[Definition]
# Match l'endpoint AJAX standard
failregex = ^<HOST> \S+ \S+ \[[^\]]+\] "(GET|POST) \/module\/ps_facetedsearch\/search(\?|\s).*" (200|301|302|400|403|404|429) .*$

# Match l'ancien endpoint encore vu sur certains thèmes/modules
            ^<HOST> \S+ \S+ \[[^\]]+\] "(GET|POST) \/modules\/ps_facetedsearch\/ps_facetedsearch-ajax\.php(\?|\s).*" (200|301|302|400|403|404|429) .*$

# Match les pages (catégories, recherche, etc.) avec paramètre q=
            ^<HOST> \S+ \S+ \[[^\]]+\] "GET \/[^\s\?]+\?[^\s\"]*\bq=[^\s\"]+.*" (200|301|302|400|403|404|429) .*$

ignoreregex =

Pour éviter une erreur classique : vérifiez que votre log contient bien la query string dans le champ “request”. Dans beaucoup de formats Nginx/Apache, c’est le cas (la “request line” loggée inclut ?…), mais pas toujours si vous avez customisé vos logs.

Exemple de ligne Nginx (indicative) que ce filtre vise :

  • 1.2.3.4 - - [date] "GET /12-chemises?q=Couleur-Rouge HTTP/1.1" 200 ...

Puis déclarez un jail (un profil de ban) avec un seuil cohérent. Fichier : /etc/fail2ban/jail.d/prestashop-facets.local

[prestashop-facets]
enabled = true
filter  = prestashop-facets
# Adaptez selon votre serveur web
logpath = /var/log/nginx/access.log
# Réglages typiques : 60s de fenêtre, 60 hits = ban 1h
findtime = 60
maxretry = 60
bantime  = 3600
backend  = auto
port     = http,https
# Debian 12 préfère nftables (sinon iptables-multiport)
action = nftables-multiport

# Allowlist minimale (exemple) : IPs internes, VPN, IP de monitoring
ignoreip = 127.0.0.1/8 ::1

Deux ajustements souvent utiles en production (sans complexifier à l’excès) :

  • Séparer les seuils : l’endpoint /module/ps_facetedsearch/search peut justifier un seuil plus strict que les pages catégorie avec q= (car il est plus “programmable” et plus cher).
  • Commencer permissif, durcir ensuite : activez avec maxretry élevé pendant 24–48h, observez, puis baissez.

Validez les regex avant de recharger (sinon vous allez bannir n’importe quoi) :

fail2ban-regex /var/log/nginx/access.log /etc/fail2ban/filter.d/prestashop-facets.conf
systemctl restart fail2ban
fail2ban-client status prestashop-facets

Réduction des faux positifs : reverse proxy, IPv6, bots légitimes, et seuils réalistes

Si votre boutique est derrière Cloudflare, Fastly, OVH CDN, un HAProxy, ou un Ingress Kubernetes, le point critique est l’IP réelle. Sans real_ip_header (Nginx) / RemoteIPHeader (Apache), fail2ban verra l’IP du proxy, et bannera le proxy. Dans Nginx, la base ressemble à : real_ip_header CF-Connecting-IP; set_real_ip_from <CIDR Cloudflare>; real_ip_recursive on; (à adapter selon fournisseur). Tant que vos logs ne contiennent pas la vraie IP, n’activez pas le jail.

Deux précautions importantes (souvent oubliées) :

  • Ne faites confiance qu’aux IPs de votre proxy/CDN pour les headers “real IP”. Sinon, un client peut forger le header et vous faire bannir des IPs arbitraires (ou contourner vos règles).
  • Validez côté logs : avant fail2ban, logguez temporairement $remote_addr et le header real IP dans un format dédié pour confirmer que la bonne adresse “tombe” au bon endroit.

Les seuils doivent être pensés “trafic réel + pics”. Une navigation facettée humaine ne génère pas 60 requêtes/minute sur /module/ps_facetedsearch/search depuis la même IP… sauf cas très spécifiques (tests QA, bots internes, proxys d’entreprise). Si vous avez un B2B avec proxy sortant partagé, une seule IP peut représenter 50 utilisateurs : dans ce cas, fail2ban à l’IP est un mauvais outil, et il faut passer au rate limiting par cookie/session ou au WAF avec rules plus contextuelles.

Point annexe : IPv6 augmente la dispersion d’IP côté attaquant, donc fail2ban perd en efficacité face à des bots “modernes” qui tournent sur des ranges. Dans la pratique, vous le voyez quand :

  • les requêtes abusives restent stables en volume,
  • mais les top IPs changent en permanence (aucune IP ne dépasse votre seuil).

Bots légitimes : le sujet le plus sensible est Googlebot (et, selon votre marché, Bingbot). Bloquer un crawler légitime peut être contre‑productif ; mais ne rien faire peut vous exposer à une exploration massive d’URLs facettées peu utiles. Si vous suspectez Googlebot, évitez les bans “au doigt mouillé” : la méthode robuste passe par une vérification DNS (reverse + forward) avant d’exclure une IP de vos actions automatiques. Dans la majorité des cas, les abus sur facettes viennent plutôt de scrapers/agrégateurs que des moteurs principaux.

Pour limiter les dégâts, combinez : (1) un bantime court au début (5–15 min) puis un jail “récidive” plus long, (2) une allowlist stricte de vos IPs de bureau/VPN, (3) une surveillance des bans pour détecter des faux positifs. Sur ce dernier point, vous avez intérêt à structurer votre monitoring comme un produit : journaux exploitables, seuils, et réduction des alertes inutiles (cf. Surveillance PrestaShop : tableau de bord, seuils et réduction des fausses alertes).

Mesurer l’impact et garder une posture SEO propre (sans casser l’indexation)

Bloquer “la navigation à facettes” a un double effet : protection perf/sécu, mais risque SEO si vous bannissez des crawlers légitimes ou si vous empêchez l’exploration de pages qui convertissent. La mitigation SEO (canonicals, noindex, paramètres d’URL, robots.txt) n’est pas le sujet principal ici, mais elle conditionne la pression crawler. En pratique, la sécurité et le SEO se touchent : une facette mal gérée peut créer des milliers d’URLs “crawlables” et amplifier le bruit. Pour une méthode structurée (contrôle qualité avant publication, détection d’URLs inutiles, etc.), vous pouvez recouper avec Audit SEO technique : contrôle qualité avant publication des pages web.

Sur la mesure purement technique, ne vous contentez pas de “moins de charge”. Fixez des KPIs, et surtout mesurez avant/après sur la même fenêtre (même jour de semaine, idéalement même plage horaire) :

  • TTFB p95/p99 sur pages catégorie et endpoint /module/ps_facetedsearch/search.
  • CPU PHP‑FPM (utilisation moyenne + saturation), longueur de file, pm.max_children atteint ou non.
  • MySQL : Threads_running, QPS, slow query log (volume et top fingerprints).
  • Code HTTP : hausse de 429/403 (attendu), baisse des 500/502/504.

Deux mini‑checks “terrain” qui évitent de se raconter des histoires :

  • Comparer le coût unitaire : si vous logguez request_time (Nginx) ou D/%D (Apache), sortez la latence médiane des requêtes facettées. Une baisse du volume est bien, mais si chaque requête restante coûte toujours 800 ms, vous avez encore un levier (cache, SQL, configuration facettes).
  • Contrôler l’effet sur le business : hausse des bans ≠ amélioration. Vérifiez aussi : taux de conversion, erreurs front, plaintes “je suis bloqué”, et taux de rebond sur catégories.

Cas réel typique (catalogue ~60k produits, 18 facettes actives) : bot à ~8–12 req/s sur /module/ps_facetedsearch/search pendant 20 minutes → CPU PHP‑FPM à 300–450% (4 vCPU), TTFB p95 catégories > 2,5 s, et hausse des erreurs 504 côté reverse proxy. Après mise en place d’un jail fail2ban (findtime 60s, maxretry 60, bantime 1h) + correction du real_ip_header, le volume de requêtes sur l’endpoint a chuté de ~90% sur l’heure suivante (les IPs tournantes restent un souci), et le TTFB p95 est revenu sous 250–400 ms. Le point important : fail2ban a “gagné du temps” pour corriger la cause (SEO, rate limiting, cache, index SQL), pas “résolu” le problème structurel.

Enfin, gardez en tête un aspect “conformité” fréquent en France/UE : les logs sont des données potentiellement personnelles (IP). Assurez‑vous d’avoir une politique de rétention, d’accès et de purge cohérente (et documentée), surtout si vous exportez des bans/logs vers un outil tiers.

Checklist de déploiement (production) pour éviter les erreurs classiques

Commencez par le chemin critique : logs exploitables, IP réelle, puis règles. Si vous êtes en Nginx, vérifiez le format et la présence de la query string ($request ou $request_uri) ; si vous êtes en Apache, vérifiez que le champ request contient bien le ?q= dans vos LogFormat. Sans ça, votre filtre ne matchera jamais, ou matchera trop largement. Pensez aussi à isoler le log FO (access) du log BO si vous avez des vhosts distincts.

Checklist rapide (ordre recommandé) :

  • [ ] Confirmer que l’endpoint est bien celui utilisé par votre thème (AJAX / non‑AJAX).
  • [ ] Vérifier que l’IP client réelle est loggée (CDN/proxy) et que vous ne faites confiance aux headers real IP que depuis des ranges connus.
  • [ ] Identifier une baseline : volume/minute sur /module/ps_facetedsearch/search et sur les URLs avec q=.
  • [ ] Écrire le filtre fail2ban, puis le tester avec fail2ban-regex (sur un log réel).
  • [ ] Démarrer avec des seuils hauts (maxretry élevé, bantime court) et observer.
  • [ ] Préparer l’allowlist : IP bureau, VPN, supervision, prestataires (si besoin), passerelles de paiement si vous avez des callbacks HTTP (à auditer).
  • [ ] Mettre en place un suivi : nombre de bans/jour, top IPs bannies, taux de 403/429, et corrélation avec CPU/DB.

Ensuite, testez de façon reproductible : (1) fail2ban-regex sur un échantillon de log, (2) activation du jail avec des seuils volontairement élevés au début, (3) observation des bans, (4) baisse progressive des seuils. Gardez une commande de rollback immédiat : fail2ban-client stop (ou fail2ban-client set prestashop-facets unbanip <IP>).

Un durcissement serveur plus global (SSL, headers, .htaccess, etc.) se traite à part, mais il faut le faire : Sécurité PrestaShop : mises à jour, SSL et durcissement .htaccess est un bon socle. Dans la même logique, si vous cherchez une défense plus “en profondeur” (et pas seulement réactive), gardez en tête l’articulation WAF + journalisation évoquée dans ce guide sur la protection et la centralisation des logs PrestaShop.

Enfin, formalisez le runbook : qui valide un ban, comment remonter une plainte client (“je suis bloqué”), comment identifier une IP partagée, comment whitelister temporairement, et comment corréler avec la charge. Sans ce cadre, fail2ban devient un pansement opaque qui masque les causes racines (facettes trop nombreuses, endpoints non cachés, absence de rate limiting, index MySQL inadaptés). Et ça, sur PrestaShop, finit toujours par ressortir au pire moment (soldes, campagne Ads, ou pic saisonnier).


À lire aussi