Cette étude de cas est publiée avec le client anonymisé. Les volumes, les signatures et le raisonnement sont reproduits tels que mesurés ; le nom de domaine, les pages ciblées et les identifiants des règles en place ne le sont pas, la campagne étant toujours active au moment de la rédaction. Les captures d'écran du dashboard ont été recadrées dans le même but.
Le site et le symptôme
Une place de marché européenne sur le plan Cloudflare Business : un catalogue public de plusieurs centaines de milliers de pages d'offres, environ cinq millions de requêtes par jour, et une application mobile qui interroge le même domaine. L'exploitant a vu monter un pic de visiteurs uniques sur le dashboard Cloudflare et a demandé si c'était une bonne ou une mauvaise nouvelle.
Jour Requêtes Pages vues Visiteurs uniques
J-4 5,33 M 443 219 43 309
J-3 5,23 M 429 719 44 173
J-2 4,96 M 409 933 64 532
J-1 2,37 M 230 294 47 137
J 3,27 M 208 304 816 627
J+1 6,28 M 444 606 721 136
J+2 6,03 M 460 856 522 820
J+3 5,48 M 420 887 378 014
Le diagnostic tient presque entier dans la forme de ces trois colonnes. Les visiteurs uniques sont multipliés par huit à dix-neuf selon le jour comparé. Les pages vues, elles, ne bougent pas, et le jour J elles baissent même. Un trafic qui arrive sans générer de pages vues est un trafic qui ne navigue pas : il récupère une page, il repart, il revient sous une autre adresse. La série horaire a ensuite daté la bascule au créneau près, de 30 223 requêtes à 02h00 UTC à 80 908 à 03h00, sans jamais redescendre ensuite.
Pourquoi les lectures habituelles ne montraient rien
Trois choses distinctes masquaient cette campagne, et chacune est assez répandue pour mériter d'être nommée.
- Les réseaux sources ressemblaient à la clientèle. Sur un échantillon de quatre heures, les principaux réseaux sources étaient un opérateur vietnamien, trois fournisseurs d'accès grand public français, un opérateur roumain et portugais, un fournisseur émirati, un opérateur historique marocain, un opérateur brésilien, un pakistanais, un turc et un mexicain, les hébergeurs cloud comme Google, Microsoft et Amazon ne venant qu'ensuite. Chacun de ces réseaux est plausible pour un site qui a du vrai trafic sur ces marchés. Les pools de bots résidentiels sont vendus précisément pour qu'une lecture par pays ou par ASN ne retourne rien.
- Le compteur de menaces était déjà élevé, et déjà expliqué. La zone disposait d'une règle personnalisée contre le scraping qui avait intercepté plus de 1,5 million de requêtes en deux jours. Voir un grand nombre monter sur un compteur censé monter n'est pas une alerte. Cela se lit comme la protection qui fait son travail, pas comme la preuve qu'une seconde campagne passe à côté.
- Le serveur d'origine allait bien. Aucune alerte serveur, aucune page lente, aucun pic processeur. Ce n'est pas un hasard, et on y revient plus bas : au bout de deux jours, la campagne a déplacé l'essentiel de son volume dans le cache edge, sans jamais réveiller la supervision de l'origine.
Le croisement qui a rendu la campagne visible
Plutôt que de découper par pays ou par réseau, nous avons croisé quatre dimensions dans une seule requête : le navigateur tel que Cloudflare le reconnaît, la version du protocole HTTP, la version TLS et la source de la classification bot de Cloudflare. Sur une fenêtre de quatre heures, restreinte aux requêtes qui atteignaient réellement l'origine :
191 225 Chrome HTTP/1.1 TLSv1.3 heuristics
10 966 Chrome HTTP/1.1 TLSv1.3 machine_learning
6 944 Chrome HTTP/2 TLSv1.3 machine_learning
Le même bucket, aux mêmes heures, la veille : 4 444 requêtes. Un facteur quarante-trois, en une ligne. Trois éléments convergent vers la même lecture.
- Une contradiction protocolaire. Un vrai Chrome négocie HTTP/2 via ALPN face à un edge Cloudflare et ne redescend pas spontanément en HTTP/1.1. « Chrome en HTTP/1.1 » est la signature d'une bibliothèque HTTP, qu'il s'agisse de Go
net/http, de Pythonrequests, decurlou denode-fetch, portant une chaîne User-Agent de Chrome. - Une classification déterministe. Cloudflare étiquetait ce trafic en
heuristics, c'est-à-dire correspondance avec une signature d'automatisation connue, et non enmachine_learning, qui est le scoring probabiliste appliqué au trafic ambigu. L'edge avait déjà reconnu la forme ; rien n'était configuré pour en tirer une conséquence. - Un pool de user agents codé en dur. Quatorze chaînes distinctes, portant chacune entre 14 059 et 14 451 requêtes. Du vrai trafic n'est jamais réparti aussi platement ; trois pour cent d'écart sur quatorze valeurs, c'est un parcours cyclique dans une liste. Les versions annoncées étaient anciennes et incohérentes entre elles, Chrome 99 à 116 sur Windows et 119 à 136 sur Mac, alors que le trafic légitime du site était dominé par Chrome 152.
Deuxième discriminant : le referer absent
En restreignant la même fenêtre à Chrome en HTTP/1.1 classé heuristics, puis en regroupant par host de referer :
191 228 aucun referer (99,99 %)
14 referer présent
Une navigation réelle vers une page profonde de catalogue porte presque toujours un referer, interne ou externe. Ce pool n'en envoyait jamais. C'est important pour la suite : le signal referer survit à un passage en HTTP/2, ce que la clause protocolaire ne fait pas. Il n'est pas exempt de faux positifs, puisqu'il attrape aussi l'entrée directe par saisie d'URL ou favori, mesurée à environ 3 500 requêtes sur vingt heures sur cette zone. C'est exactement pour cela qu'on le mesure avant de l'appliquer.
Plat jour et nuit, puis absorbé par le cache
Trois jours après la bascule, la campagne tournait toujours, mais elle n'avait plus le même profil selon l'endroit d'où on l'observait. Sur le trafic total, le bucket suspect restait plat : environ 25 000 requêtes par heure de jour comme de nuit, 471 370 requêtes sur la journée, soit 8,5 % du trafic de la zone, sans trace de cycle jour/nuit, avec un creux inexpliqué entre 09h00 et 12h00 UTC. Le même jour, le trafic légitime variait d'un facteur six entre son creux de 02h00 et son pic de 14h00. Une courbe qui ignore celui de sa propre audience est de l'automatisation, à elle seule, sans autre preuve.
Sur le trafic qui atteignait réellement l'origine, en revanche, la même courbe s'était effondrée après deux jours de plateau : la majeure partie du balayage était désormais servie par le cache edge, sur des pages déjà demandées. Rien n'avait changé côté attaquant, c'est l'edge qui absorbait la charge. C'est le point qui piège le plus d'exploitants : face à un catalogue public mis en cache, le scraping peut continuer à pleine vitesse sans jamais faire bouger une seule courbe de supervision serveur.
Deux pièges sur le chemin de la correction
Un rate limiting par URL ne sert à rien ici
Sur les 1 544 207 requêtes que la règle existante avait bloquées en deux jours, les cinquante chemins les plus touchés en représentaient moins de 55 700, soit moins de quatre pour cent du volume. Le reste était une longue traîne de pages de détail énumérées par identifiant séquentiel sur une plage d'environ 39 000 valeurs, chaque page individuelle se situant à 400 à 430 hits en deux jours. Chaque URL restait en dessous de tout seuil raisonnablement paramétrable : un rate limiting par URL n'aurait donc rien intercepté du tout. Le comptage doit être agrégé par motif d'URI ou par IP source. C'est la raison la plus fréquente pour laquelle une règle de rate limiting part en production et ne change rien.
Lire les actions avant de lire les volumes
Skip porte davantage de trafic que la règle de blocage ; son volume n'en fait ni une faille ni une candidate au resserrement. Les volumes affichés portent sur une autre fenêtre que celles citées dans le texte.
La zone comportait une seconde règle qui correspondait à plus de 1,4 million de requêtes sur les endpoints de l'application mobile. Jugée sur le seul volume, elle ressemble à une masse de trafic non filtré et à une candidate au resserrement. C'était en réalité une liste d'autorisation Skip délibérée, posée pour que l'application mobile ne rencontre jamais de Managed Challenge. La passer en blocage aurait mis l'application hors service pour tous les clients. Nous l'avions signalée comme candidate en cours d'analyse et avons retiré la suggestion dès que nous avons identifié quelle règle portait quelle action. Les compteurs d'événements disent ce qu'une règle a intercepté, jamais ce qu'elle en a fait ; sur une zone que l'on n'a pas configurée soi-même, établir l'action de chaque règle vient avant toute proposition d'y toucher.
La recommandation : quatre couches, pas une règle
La règle protocolaire arrête cette campagne, pas cet attaquant. Le jour où l'opérateur bascule sa bibliothèque en HTTP/2, ce que curl --http2, Python httpx et Go net/http font tous nativement, la contradiction disparaît et la règle n'intercepte plus rien. La superposition n'est pas une ceinture et des bretelles : c'est la seule approche qui reste vraie face à un pool de bots à rotation d'adresses, parce qu'elle s'attaque à l'économie de l'attaque. Chaque couche rend chaque nouvelle adresse IP plus coûteuse, et c'est l'attaquant qui paie ce trafic.
- Couche 1, la contradiction protocolaire.
http.user_agent contains "Chrome" and http.request.version eq "HTTP/1.1", en action Managed Challenge plutôt qu'en blocage : certains proxies d'entreprise rétrogradent effectivement en HTTP/1.1, et un vrai navigateur derrière l'un d'eux résoudra le CAPTCHA là où un client Go échouera. L'application mobile n'est pas concernée, son user agent ne contient aucune chaîne de navigateur, et le trafic Android WebView parle HTTP/2 et échappe à la clause. - Couche 2, un verrou sur la section ciblée. Les requêtes vers la section énumérée sans cookie
cf_clearancereçoivent un Managed Challenge. C'est le levier principal contre la rotation d'IP : chaque nouvelle adresse doit résoudre un CAPTCHA avant de voir la moindre page, ce qui rend très cher un pool de bots résidentiels facturé au trafic. Il faut y ajouter une clause exemptant le cookie de session de l'application, pour que les clients connectés ne rencontrent pas un CAPTCHA dès leur premier clic. - Couche 3, un rate limiting agrégé par IP sur le motif énuméré. Le comptage porte sur le motif d'URI et non sur l'URL exacte, avec un seuil de quinze à vingt requêtes par IP sur dix minutes, en action Managed Challenge. Un humain consulte quelques offres ; un balayage séquentiel sur des dizaines de milliers d'identifiants franchit ce seuil même à cadence volontairement lente.
- Couche 4, le pool de user agents lui-même. Énumérer les quatorze chaînes codées en dur ne coûte rien, attrape l'attaquant s'il change de protocole sans renouveler son pool, et présente un résidu légitime quasi nul vu l'obsolescence des versions annoncées. C'est fragile par construction, le pool pouvant être régénéré en un après-midi, et cela vaut d'être déployé quand même puisque c'est gratuit.
Sous ces quatre couches, le Super Bot Fight Mode réglé pour intercepter le trafic « definitely automated » sert de filet si tout le reste est contourné. Et chacune de ces règles part d'abord en action Log, pendant quelques heures, pour que le résidu légitime soit mesuré par rapport à la baseline avant toute application. Un changement à la fois, jamais plusieurs, sinon aucun effet ne peut être attribué à sa cause.
Le point qui passait avant toutes les règles
Les pages énumérées se trouvaient sous un chemin de compte et étaient adressées par identifiant séquentiel. Si des pages de ce type sont servies sans vérifier que l'enregistrement appartient à l'utilisateur authentifié, il existe un problème de contrôle d'accès indépendant du volume de trafic, qu'aucun filtrage au niveau du bord ne corrigera : un attaquant qui ralentit à une requête par minute parcourt quand même tout le catalogue. Le vérifier passait avant le déploiement de la moindre security rule, et nous l'avons dit avant de proposer les règles. Si ces pages exigent une authentification, le scraping devient sans objet et les quatre couches deviennent un confort plutôt qu'une nécessité.
C'est un schéma récurrent qui mérite d'être énoncé clairement. Le filtrage au bord achète du temps et renchérit l'attaque ; il ne remplace pas un contrôle d'autorisation. Quand un diagnostic met au jour quelque chose que l'application devrait faire à la place, le dire fait partie du travail, même si cela raccourcit la mission.
Ce que ce cas montre
- Des visiteurs uniques qui montent à pages vues constantes sont une signature. Un trafic qui ne navigue pas n'est pas une audience. Avant d'enquêter sur quoi que ce soit d'autre, comparez ces deux courbes.
- La géographie de la source est la première chose qu'un attaquant dilue. Les lectures par pays et par réseau se déjouent à peu de frais avec un pool de bots résidentiels. La cohérence protocolaire, c'est-à-dire l'accord entre le navigateur annoncé, la version HTTP négociée et les en-têtes réellement envoyés, est bien plus difficile à falsifier et bien plus discriminante.
- Un serveur d'origine calme ne prouve rien. Face à un catalogue public mis en cache, le scraping est servi par le bord et n'apparaît jamais dans la supervision serveur. Ici, la campagne a fini par tirer l'essentiel de ses requêtes du cache, sans changer un seul paramètre.
- Mesurer en Log, puis appliquer. Le coût d'une security rule mal calibrée est payé par de vrais clients, dans le monde entier, quelques secondes après le déploiement. Quelques heures de mesure sont une assurance bon marché.
Si vous observez la même forme sur votre propre zone, les deux guides ci-dessous traitent les parties stables de ce travail sous forme de tutoriel : rate limiting et règles bots sur les endpoints sensibles, et séparer les robots que vous voulez de ceux que vous ne voulez pas.
Questions fréquentes
Pourquoi la lecture par pays et par ASN ne montrait-elle rien ?
Parce que la campagne passait par des bots résidentiels à rotation d'adresses, situés dans les marchés réellement desservis par le site. Les opérateurs grand public portaient davantage de trafic que les réseaux de datacenters, si bien que toute lecture géographique ou réseau paraissait plausible. La localisation de la source est la première dimension qu'un attaquant dilue, parce que c'est la moins chère à falsifier. La cohérence protocolaire, elle, est bien plus difficile à maquiller : c'est le croisement navigateur, version HTTP et version TLS qui a rendu la campagne lisible en une seule requête.
Pourquoi un user agent de navigateur en HTTP/1.1 est-il un signal fiable ?
Un vrai Chrome négocie HTTP/2 via ALPN face à un edge Cloudflare et ne redescend pas spontanément en HTTP/1.1. Un user agent qui annonce Chrome tout en parlant HTTP/1.1 est presque toujours une bibliothèque HTTP portant une chaîne de navigateur. Le signal n'est pas parfait : certains proxies d'entreprise rétrogradent les connexions, et c'est précisément pour cela que la règle doit appliquer un Managed Challenge plutôt qu'un blocage sec.
Pourquoi un rate limiting par URL échoue-t-il face à un scraper ?
Parce qu'un scraper qui énumère un catalogue répartit ses requêtes sur des dizaines de milliers d'URL distinctes. Ici, les cinquante chemins les plus touchés représentaient moins de quatre pour cent du volume bloqué, et chaque page individuelle restait à quelques centaines de hits sur deux jours, très en dessous de tout seuil raisonnable. Le comptage doit être agrégé par motif d'URI ou par IP source, jamais par URL exacte.
La charge du serveur d'origine indique-t-elle qu'on se fait scraper ?
Non. Dès que le cache edge est chaud sur un catalogue public, la plupart des requêtes de scraping sont servies par Cloudflare et n'atteignent jamais l'origine. La supervision serveur reste silencieuse pendant que le contenu est aspiré à pleine vitesse. Dans ce cas précis, la campagne a même déplacé l'essentiel de son volume dans le cache après deux jours, sans changer un seul paramètre de son côté.
CF Garage