Sécurité

Botnet sur la recherche à facettes : le cookie-gate qui a fait tomber la charge de 70 à 2,5 en deux minutes

2026-10-07 · Pierre

14:53 Un ticket arrive d'une agence partenaire : « Notre client et nous-mêmes constatons d'importants ralentissements sur le serveur. Pouvez-vous effectuer un diagnostic ? »
17:51 La charge est retombée à 2,5. Entre les deux, une après-midi que nous allons vous raconter en détail, parce que le dénouement tient en trois lignes de configuration que vous pouvez poser ce soir sur votre propre boutique.

Il y a trois semaines, nous expliquions pourquoi la recherche à facettes transforme une boutique en buffet à volonté pour les robots. Cet article est la suite pratique : le jour où le buffet a été pris d'assaut, et le geste qui l'a fermé.

Résumé en 10 secondes

Un botnet résidentiel distribué a saturé une boutique PrestaShop en enchaînant des URLs de filtres. 5 543 adresses IP pour 6 000 requêtes, donc ni ban IP ni fail2ban. Les bots passaient sous les seuils de nombre de filtres, et le cache ne sert à rien sur des combinaisons uniques. La solution : refuser toute URL de facette qui n'est pas accompagnée d'un cookie de session. Un vrai visiteur a toujours chargé une page avant de cliquer sur un filtre. Un bot, non. Load de 50-70 à 2,5 en deux minutes, trafic légitime intact.

5 543adresses IP distinctes pour environ 6 000 requêtes
92 %des IP n'ont fait qu'une seule requête
3 lignesde configuration Apache pour fermer la porte
70 → 2,5load average, en deux minutes après déploiement

Ce que montraient les logs

La boutique est un PrestaShop multilingue de mobilier, avec une navigation à facettes réellement utilisée par les clients : couleur, essence de bois, prix, tri. Le serveur affichait un load average autour de 200 à l'ouverture du ticket, pour une machine qui tourne d'habitude entre 1 et 3. PHP-FPM et MariaDB étaient saturés, les pages mettaient dix à vingt secondes à répondre.

Dans les logs d'accès, une signature très homogène. Des requêtes sur les pages catégories avec le paramètre q= du module de facettes, chargé de quatre ou cinq valeurs de filtres combinées, dans les trois langues de la boutique. Voici à quoi ressemblaient ces lignes, anonymisées :

91.149.x.x  "GET /es/87-sillones?order=product.position.asc&q=Color-Amarillo+mostaza-Azul+tormenta-Beige-ladrillo%2FEsencia+de+madera-Roble"
            referer: /es/87-sillones?order=product.position.asc&q=Color-Azul+tormenta-Beige-ladrillo%2FEsencia+de+madera-Roble
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) ... Chrome/142.0.0.0 Safari/537.36"
217.18.x.x  "GET /es/87-sillones?order=product.position.asc&q=Color-Ardilla-Azul+denim-Azul+Luis-Azul+pavo+real-beige-camello-Negro-Con+motivos"
            referer: /es/87-sillones?order=product.position.asc&q=Color-Ardilla-Azul+denim-Azul+Luis-Azul+pavo+real-beige-camello-Negro
            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ... Chrome/144.0.0.0 Safari/537.36"
169.224.x.x "GET /it/87-poltrone?SubmitCurrency=1&id_currency=1&order=product.price.desc&q=Colore-a+motivi-Giallo+senape-beige%2FEssenza+di+legno-Quercia"
            referer: /it/87-poltrone?...&q=Colore-a+motivi-Camello-Cammello-Giallo+senape%2FEssenza+di+legno-Quercia
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) ... Chrome/144.0.0.0 Safari/537.36 Edg/144.0.0.0"

Trois détails rendent ce trafic difficile à filtrer. Les user-agents sont ceux de navigateurs récents, Chrome 142 et 144, Edge, sur Windows et macOS, rien d'un crawler déclaré. Les referers sont cohérents : chaque requête prétend venir de la page de filtres précédente, avec une valeur de moins, comme si quelqu'un ajoutait un filtre à chaque clic. Et les adresses IP viennent du monde entier, chez des opérateurs grand public. C'est la signature d'un botnet résidentiel, loué à l'heure, qui exécute un parcours de navigation scripté.

Ce qui n'a pas marché

Nous avons d'abord fait ce que tout le monde fait, et c'est utile de dire pourquoi chaque étape a échoué.

MesureRésultatPourquoi
Ban IP, fail2banInutile5 543 IP pour 6 000 requêtes. 92 % des adresses n'apparaissent qu'une fois. Bannir une IP qui ne reviendra pas ne sert à rien, et la liste de bannissement grossit plus vite que le botnet ne tourne.
Rate limiting par IPInutileMême raison : à une requête par IP, aucun compteur ne dépasse jamais le seuil. Le botnet est conçu pour ça.
Seuil sur le nombre de filtresPartielLe vhost refusait déjà les URLs à six valeurs de filtres ou plus. Durcir ce seuil a fait baisser la charge une première fois, mais les bots sont restés à quatre ou cinq valeurs, exactement comme un client humain. Descendre encore aurait cassé la navigation réelle.
Cache pleine pageSans effetChaque combinaison de filtres est quasi unique. Une page que personne n'a demandée avant et que personne ne redemandera après ne sort jamais du cache : chaque hit descend jusqu'à MariaDB.
Captcha, CSRFNon applicableUn catalogue est public, un captcha sur la navigation pénalise les clients et le référencement. Le CSRF protège les actions, pas la lecture de pages en GET. Nous l'avions déjà expliqué dans l'article précédent.

Après le durcissement des seuils, il restait environ 13 requêtes par seconde qui passaient en 200 sur les facettes. Ça paraît peu. C'est suffisant pour saturer un PHP-FPM dont chaque requête de filtrage prend une à deux secondes de base de données : treize par seconde multiplié par deux secondes, c'est vingt-six workers occupés en permanence. La charge est redescendue de 200 à une fourchette de 50 à 70, et s'y est installée.

L'idée : un humain a toujours un cookie

La question qui débloque tout est celle-ci : qu'est-ce qu'un vrai client a, que ce botnet n'a pas ?

Un visiteur réel qui clique sur un filtre a forcément chargé une page avant. La page catégorie, la page d'accueil, une fiche produit. Dès cette première page, PrestaShop lui a posé un cookie de session. Quand il clique ensuite sur « Couleur : bleu », son navigateur renvoie ce cookie avec la requête de filtre. Il n'existe pas de parcours humain normal qui commence directement par une URL de facette à quatre filtres, sans aucun cookie.

Le botnet, lui, attaque les URLs de facettes directement. Il forge le referer, mais il ne s'embarrasse pas de jouer la session : pas de cookie. C'est le discriminant. Il ne repose ni sur l'IP, ni sur le user-agent, ni sur un seuil arbitraire, mais sur un comportement que les humains ont tous et que ces bots n'avaient pas.

La règle, en trois lignes

  • Si la requête contient le paramètre de recherche à facettes dans sa query string,
  • et qu'elle n'est accompagnée d'aucun cookie de session de l'application,
  • alors on répond 429 immédiatement, avant même que PHP soit sollicité.

La configuration Apache

Déployée dans le vhost, avant les règles de seuil existantes, après sauvegarde du fichier :

# Cookie-gate sur la recherche à facettes PrestaShop
RewriteEngine On
RewriteCond %{QUERY_STRING} "(^|&)q=" [NC]
RewriteCond %{HTTP_COOKIE} !(PHPSESSID|PrestaShop-) [NC]
RewriteRule ^ - [R=429,L]

Lecture ligne à ligne. La première condition cible les URLs dont la query string contient un paramètre q=, en début de chaîne ou après un &. C'est le paramètre du module de navigation à facettes de PrestaShop. La deuxième condition est la négation : elle est vraie si l'en-tête Cookie ne contient ni PHPSESSID, ni un cookie commençant par PrestaShop-, le préfixe des cookies de session de la boutique. Si les deux conditions sont vraies, la règle renvoie un 429 « Too Many Requests » et s'arrête là.

Le choix du 429 plutôt qu'un 403 n'est pas anodin. Sémantiquement, on dit au client « vous allez trop vite, revenez plus tard », ce qui est exactement le message qu'un robot bien écrit est censé comprendre. Et nous avons observé un effet secondaire bienvenu : le botnet a décroché de lui-même au bout de quelques minutes de 429. Quand un parcours scripté n'obtient plus que des erreurs, l'opérateur qui loue le botnet passe à une autre cible. Un 403 aurait probablement eu le même effet, mais le 429 est plus honnête sur l'intention.

Le résultat a été immédiat : le spam résiduel est passé de 13 requêtes par seconde à zéro, le load average de 50-70 à 2,5 en deux minutes. Les pages ont retrouvé leur temps de réponse habituel.

Adapter à votre application

Le principe se transpose à n'importe quel CMS, en changeant deux choses : le paramètre d'URL de vos filtres et le nom du cookie de session.

ApplicationParamètre de filtres (exemples)Cookie de session
PrestaShop 1.7 / 8q=PHPSESSID, PrestaShop-*
WooCommercefilter_color=, filter_size=, min_price=wordpress_*, woocommerce_session_*, wp_woocommerce_session_*
Magento 2color=, size=, price= selon les attributsPHPSESSID, mage-cache-sessid
Drupal + Facetsf[0]=, f[1]=SESS*, SSESS*
Symfony / Laravel sur mesurevos propres paramètresPHPSESSID ou le nom défini dans la config

L'équivalent Nginx s'écrit avec une variable intermédiaire, puisque Nginx ne chaîne pas les conditions :

# Cookie-gate facettes, variante Nginx
set $facet_nocookie 0;
if ($args ~* "(^|&)q=")                    { set $facet_nocookie 1; }
if ($http_cookie ~* "(PHPSESSID|PrestaShop-)") { set $facet_nocookie 0; }
if ($facet_nocookie = 1)                     { return 429; }

Tester avant et après

Trois requêtes suffisent pour valider la règle sans risque. Une URL de facette sans cookie doit renvoyer 429. La même URL avec un cookie de session doit renvoyer 200. Une page normale sans query string, sans cookie, doit renvoyer 200 : c'est le cas du premier visiteur, et il ne doit jamais être bloqué.

# 1. Facette sans cookie : attendu 429
curl -s -o /dev/null -w "%{http_code}\n" \
  "https://votre-boutique.fr/fr/12-categorie?q=Couleur-Bleu"

# 2. Facette avec cookie de session : attendu 200
curl -s -o /dev/null -w "%{http_code}\n" \
  -H "Cookie: PHPSESSID=test" \
  "https://votre-boutique.fr/fr/12-categorie?q=Couleur-Bleu"

# 3. Page catégorie sans filtre, sans cookie : attendu 200
curl -s -o /dev/null -w "%{http_code}\n" \
  "https://votre-boutique.fr/fr/12-categorie"

Puis ouvrez la boutique dans un navigateur en navigation privée, naviguez jusqu'à une catégorie, cliquez sur deux filtres. Si tout fonctionne, votre session a été posée à la première page et les filtres passent. C'est le test qui compte vraiment.

Les limites, parce qu'il y en a

Nous préférons vous les donner nous-mêmes.

Le visiteur qui arrive directement sur une URL filtrée. Si Google a indexé une page de facette et qu'un internaute clique dessus depuis les résultats, il arrive sans cookie et reçoit un 429. C'est le vrai coût de la règle. Deux façons de le réduire : d'une part interdire les URLs de filtres dans robots.txt pour qu'elles sortent de l'index, d'autre part remplacer le 429 par une redirection 302 vers la page catégorie sans filtre. Le visiteur atterrit alors sur la catégorie, avec un cookie, et peut refiltrer. Nous avons gardé le 429 ici parce que les URLs de facettes de cette boutique n'étaient pas indexées, mais vérifiez le vôtre dans la Search Console avant de choisir.

Les crawlers légitimes. Googlebot et Bingbot ne portent pas de cookie et recevront aussi un 429 sur les facettes. C'est en réalité ce que vous voulez : votre budget de crawl doit aller aux fiches produits, pas aux combinaisons de filtres. Formalisez-le dans robots.txt pour que le crawler n'essaie même pas :

User-agent: *
Disallow: /*?*q=

Le botnet qui s'adapte. Rien n'empêche un opérateur de charger d'abord la page catégorie, de récupérer le cookie, puis d'enchaîner les filtres. Ce jour-là, il ne l'a pas fait, et c'est fréquent : les scripts de scraping sont écrits pour le cas général, pas pour votre boutique. S'il revient en jouant la session, l'escalade suivante consiste à poser le cookie en JavaScript plutôt que côté serveur, ce qui exige du bot d'exécuter la page. C'est plus coûteux pour lui, et c'est tout l'enjeu : rendre votre buffet moins rentable que celui du voisin.

Les clients qui refusent les cookies. Le cookie de session est un cookie technique strictement nécessaire, exempté de consentement. Un navigateur qui bloque tous les cookies, y compris ceux-là, ne peut déjà pas commander sur la boutique. La règle ne crée pas de nouvelle exclusion.

Ce que nous en avons retenu

  1. Cherchez le discriminant comportemental, pas l'identité. IP, user-agent et referer sont des déclarations du client, falsifiables à volonté. Le cookie de session est une conséquence d'un parcours réel. Face à un botnet distribué, c'est ce genre d'invariant qu'il faut chercher.
  2. Bloquez le plus tôt possible dans la chaîne. Le 429 est émis par Apache avant PHP et avant MariaDB. Le coût d'une requête refusée est de l'ordre de la milliseconde. Un filtre équivalent dans l'application aurait quand même consommé un worker PHP.
  3. Le cache n'est pas une protection. Il amortit le trafic répétitif. Le trafic de facettes est l'exact inverse.
  4. Testez le cas du premier visiteur. La seule façon de casser une boutique avec cette règle est de l'appliquer trop large. Les trois commandes curl ci-dessus prennent une minute.

Cette règle fait désormais partie de notre check-list d'audit à l'arrivée d'une boutique en infogérance, à côté de la désactivation du module de facettes quand il ne sert pas. Si votre serveur charge sans que vos ventes ne suivent, c'est toujours le même conseil : dix minutes de logs avant tout achat de capacité.

FAQ : cookie-gate et recherche à facettes

Qu'est-ce qu'un cookie-gate ?

Une règle du serveur web qui refuse certaines URLs coûteuses, ici les pages de filtres d'une boutique, quand la requête n'est accompagnée d'aucun cookie de session. Un visiteur réel a toujours chargé une page avant de filtrer, donc il porte un cookie. Un bot qui attaque directement les URLs de filtres n'en a pas et reçoit un 429 avant que PHP ou la base de données soient sollicités.

Pourquoi bannir les adresses IP ne fonctionnait-il pas ?

Parce que le botnet était résidentiel et distribué : 5 543 adresses IP pour environ 6 000 requêtes, et 92 % des adresses n'ont fait qu'une seule requête. Aucun compteur par IP ne dépasse un seuil, et bannir une adresse qui ne reviendra pas est sans effet. fail2ban et le rate limiting par IP reposent sur la répétition, que ce type de botnet évite par construction.

Le cookie-gate bloque-t-il Googlebot ?

Sur les URLs de filtres uniquement, oui, et c'est souhaitable : le budget de crawl doit aller aux pages canoniques. Les pages catégories et fiches produits sans paramètre de filtre restent accessibles sans cookie. Complétez par un Disallow: /*?*q= dans robots.txt pour que les moteurs n'essaient même pas.

Que se passe-t-il pour un client qui arrive directement sur une URL filtrée depuis Google ?

Il n'a pas encore de cookie et reçoit un 429. C'est la principale limite. Si vos URLs de facettes sont indexées, remplacez le 429 par une redirection 302 vers la page catégorie sans filtre : le visiteur arrive sur la catégorie, reçoit son cookie, et peut filtrer normalement. Vérifiez dans la Search Console si ces URLs sont indexées avant de choisir.

Un bot ne peut-il pas simplement récupérer un cookie d'abord ?

Si. Mais les scripts de scraping sont écrits pour le cas général et la plupart ne le font pas : ce jour-là, le botnet a décroché après quelques minutes de 429. S'il s'adapte, l'escalade suivante est de poser le cookie en JavaScript, ce qui oblige le bot à exécuter la page et renchérit fortement chaque requête. L'objectif n'est pas l'imperméabilité, c'est de rendre votre site moins rentable à attaquer que le suivant.

Pour aller plus loin :
• mod_rewrite, documentation Apache · robots.txt, documentation Google
• Sur datacampus.fr : Recherche à facettes et bots IA, le buffet à volonté · Tracker les agents IA et crawlers avec Matomo · Comment on protège nos clients du DDoS · Notre infogérance

Vous avez un projet d'hébergement ?

Configurez-le en 2 minutes et recevez votre devis personnalisé sous 48 h. Sans engagement.

Configurer mon hébergement → Nous appeler

Articles sur le même sujet

← Retour au blog