Stats — tour de contrôle anti-bots pour phpBB

Depuis l’explosion des entraînements LLM, le forum subit un trafic non-humain massif : scrapers qui aspirent les contenus écrits par les membres, robots qui sondent les endpoints à la recherche de failles, IPs distribuées qui téléchargent les pièces jointes en parallèle pour ne pas déclencher les rate-limits. Stats est l’extension que j’ai développée pour observer ce trafic, le classer, et le bloquer automatiquement via fail2ban. Voici ce qu’elle fait concrètement.

Le contexte : pourquoi cette extension existe

Un phpBB n’a aucun moyen natif de savoir qui le visite vraiment. Les logs Apache montrent des hits, mais pas l’intention. Un Chrome 124 légitime et un scraper qui se fait passer pour Chrome 124 sont indistinguables d’une simple lecture du User-Agent. Pourtant la différence est énorme : l’un consomme des pages, l’autre vide l’intégralité du forum en quelques heures, parfois en distribuant les requêtes sur des centaines d’IPs IPv6 d’un même /48 pour contourner les limites par IP.

L’extension s’inscrit entre Apache et fail2ban. Elle observe chaque visite côté PHP — là où les vrais signaux sont accessibles (cookies, JS, télémétrie comportementale) — et émet des events structurés dans un fichier journal dédié. Ce journal est ensuite lu par des filtres fail2ban qui décident des bans. L’extension ne bannit jamais elle-même : elle se contente de produire un signal exploitable.

Ce qu’on voit dans l’ACP

L’ACP n’est pas un outil de reporting passif : c’est la tour de contrôle. Chaque onglet répond à une question opérationnelle.

  • Vue d’ensemble — combien de visites aujourd’hui ? Quelle proportion d’humains présumés, de cas ambigus, de bots avérés ? Le donut probabiliste est calculé par le modèle session_probability_model.php qui combine les signaux observés en une probabilité P(bot) par session. Chaque session affiche son badge.
  • Sessions — la timeline de chaque visiteur : pages vues, ordre, durée entre clics, téléchargements de pièces jointes, signaux émis, état du cookie, résultat de l’AJAX de télémétrie. C’est là qu’on voit immédiatement un scraper : 200 pages en 4 secondes, zéro mouvement de souris, pas de résolution d’écran.
  • Pages — top URLs visitées et leurs referers complets. Très utile pour repérer un site tiers qui scrape via crawler maison.
  • Carte — répartition géographique (jVectorMap). On y voit immédiatement les vagues qui partent d’un pays donné quand un nouvel acteur entre en chasse.
  • Comportements — c’est l’onglet qui change tout. Profils statistiques appris à partir des membres connectés (référence humaine), comparaison avec les invités, détection des outliers, traces SVG du curseur quand elles ont pu être capturées, et historique des cas marginaux.

Comment ça détecte : cinq couches

L’extension empile cinq familles de signaux. Aucune n’est suffisante seule : c’est leur combinaison qui donne des décisions robustes.

1. Signaux HTTP/UA — instantanés, côté serveur

Émis dès la première requête, sans attendre JS. Ce sont les plus rapides à fail2ban. Exemples : empty_ua (aucun User-Agent), fake_chrome_build (UA Chrome avec numéro de build incohérent), fake_legit_bot (prétend être Googlebot mais le reverse-DNS dément), html_entities_in_url (URL avec &amp%3B — scraper qui rejoue les liens copiés depuis le HTML source), posting_first_visit (POST sur posting.php dès le premier hit : aucun humain ne fait ça).

2. Signaux JS — télémétrie AJAX

Le navigateur exécute un script qui mesure : résolution d’écran, taille de la fenêtre, présence de la propriété navigator.webdriver, profil de scroll (vitesse, paliers, sauts), événements de souris ou de touch. Le tout est envoyé sur l’endpoint sécurisé POST /stats/px (token de lien lié à la session + contrôle same-origin). Un bot headless ne déclenche jamais ce script ou laisse des traces aberrantes : ajax_webdriver, ajax_scroll_too_fast, cursor_no_movement, no_screen_res.

3. Signaux statistiques appris — la baseline humaine

Plutôt que des seuils figés, l’extension apprend de l’activité réelle des membres connectés (qui sont par définition des humains). Distribution des vitesses de lecture, densité de scroll, ratio de sauts, fréquence d’interaction : tout est mesuré, et les signaux learn_*_outlier se déclenchent quand un visiteur s’écarte trop de cette distribution. Avantage : la baseline se recalibre toute seule à la spécificité du forum.

4. Signaux d’identité partagée — cookie & fingerprint clonés

Chaque visiteur reçoit un cookie de session signé, stocké hashé en base. Tant qu’il est valide, c’est l’ancre principale de la session — même si l’IP change (privacy extensions IPv6, NAT mobile). Si le même cookie ou la même empreinte navigateur est présenté depuis plusieurs IPs distinctes, l’extension émet guest_cookie_clone_multi_ip ou guest_fp_clone_multi_ip — quasi-certitude d’une botnet qui partage des identités volées.

5. Signaux différés — le cron geo_async

Certaines détections n’ont de sens qu’a posteriori. Le cron geo_async résout les IPs via ip-api.com (cache DB par IP et par préfixe /24 pour économiser les appels), puis fait deux choses : (a) il flague les invités venant d’un pays ciblé qui n’ont vu qu’une page sans interaction en moins de 5 minutes (cn_no_interaction_5m) ; (b) il détecte les téléchargements distribués : trop de pièces jointes vues depuis trop d’IPs proches sur une fenêtre courte → xip_dl_soft_v1 ou xip_dl_hard_v1 selon le score.

Le pont fail2ban : là où l’observation devient action

L’extension écrit deux types de lignes dans le journal /var/log/security_audit.log (chemin configurable dans l’ACP) :

2025-11-01 14:32:17 PHPBB-SIGNAL ip=1.2.3.4 session=abc123 user_id=0 signals="fake_legit_bot,no_screen_res" ua="Mozilla/5.0 ..." page="/viewtopic.php?t=42" cc=CN
2025-11-01 14:33:05 PHPBB-XIP ip=1.2.3.4 cc=DE method=xip_dl_soft_v1 severity=soft score=72 topic_id=18 downloads=9 views=0 period_sec=3600

PHPBB-SIGNAL est émis en temps réel pendant la visite. PHPBB-XIP est émis a posteriori par le cron. Les deux portent l’IP, le code pays et la liste des signaux — c’est tout ce que fail2ban a besoin de connaître.

2026-05-20 18:22:48 PHPBB-SIGNAL ip=123.21.186.XXX session=2fdcda1c…XXX user_id=0 signals="old_chrome_106" ua="Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/106.0.5249.119" page="/download/file.php?id=2817&sid=…" cc=VN
2026-05-20 18:25:59 PHPBB-SIGNAL ip=177.128.53.XXX session=b584ea20…XXX user_id=0 signals="posting_first_visit" ua="Opera/8.13.(Windows NT 4.0; mni-IN) Presto/2.9.177" page="/posting.php?mode=quote&p=201159" cc=BR
2026-05-20 18:28:21 PHPBB-SIGNAL ip=180.191.236.XXX session=346bd8f9…XXX user_id=0 signals="old_chrome_42,fake_chrome_build" ua="Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_10_0) Chrome/42.0.887.0" page="/download/file.php?id=90985" cc=PH
2026-05-20 18:32:11 PHPBB-XIP ip=192.99.7.XXX cc=CA method=xip_dl_soft_v1 severity=soft score=68 topic_id=24 downloads=11 views=0 period_sec=3600

Les filtres et jails livrés avec l’extension

Dans le dossier fail2ban/ de l’extension, 13 filtres prêts à l’emploi. Sur ce serveur, ils sont symlinkés depuis /etc/fail2ban/filter.d/ et lus par autant de jails. Voici ceux qui font le plus gros du travail :

  • phpbb-badbot-confirmed — signaux haute confiance (fake_legit_bot, posting_first_visit, fake_chrome_build, empty_ua, etc.) : 1 hit suffit, ban 24 h, escalade ×2 jusqu’à 30 jours.
  • phpbb-badbot-suspicious — signaux modérés (no_screen_res, ajax_scroll_*, learn_*_outlier) : 3 hits, ban 12 h, escalade.
  • phpbb-guest-cookie-clone / phpbb-guest-fingerprint-clone — cookie ou empreinte partagés sur plusieurs IPs : 1 hit, ban progressif jusqu’à 14 jours.
  • phpbb-cn-no-interaction — invité d’un pays ciblé qui ne reste pas : ban du /24 entier pour 3 jours (action nftables custom).
  • phpbb-crossip-soft / phpbb-crossip-hard — download distribué détecté par le cron : hard = bans longs avec backoff incrémental.
  • phpbb-combo-scraper — agrégateur qui ban si plusieurs signaux différents convergent vers la même IP sur une courte fenêtre.

Tous les jails partagent bantime.increment = true : un récidiviste voit son ban doubler à chaque rechute, jusqu’à plusieurs semaines. Les bans courts sont la norme, les bans longs sont mérités.

Ce que ça donne en exploitation sur ce serveur

Quelques chiffres relevés en production sur forum.debucquoi.com au moment d’écrire ces lignes :

  • 22 049 IPs bannies au total par phpbb-badbot-confirmed — dont 5 191 actuellement actives.
  • 462 subnets /24 bannis par phpbb-cn-no-interaction (366 actifs).
  • 44 433 events PHPBB-SIGNAL dans le journal en quelques mois.
  • 14 132 hits du signal posting_first_visit — autant de bots qui ont tenté un POST direct sur /posting.php sans naviguer.
  • ~10 000 hits cumulés sur les signaux old_chrome_* — UA forgés sur d’anciennes versions de Chrome avec build incohérent.
$ fail2ban-client status phpbb-badbot-confirmed
Status for the jail: phpbb-badbot-confirmed
|- Filter
|  |- Currently failed: 0
|  |- Total failed:     10141
|  `- File list:        /var/log/security_audit.log
`- Actions
   |- Currently banned: 5399
   |- Total banned:     22869
   `- Banned IP list:   45.155.205.XXX 185.220.101.XXX 80.82.78.XXX 91.197.91.XXX …

Ce qui fait que ça tient sans faux positifs

  • Vérification rDNS pour les bots légitimes : un UA Googlebot ou Bingbot est confronté à un reverse-DNS ; s’il ne correspond pas au domaine attendu, c’est fake_legit_bot. Les vrais crawlers Google/Bing passent.
  • Baseline humaine apprise par site : les seuils des signaux learn_* ne sont pas codés en dur, ils proviennent de la distribution réelle des membres connectés. Un forum lent et un forum rapide auront des seuils différents, automatiquement.
  • Cookie signé comme ancre stable : une session ne dépend plus de l’IP. Un mobile qui change d’IP toutes les 30 min n’est pas pris pour deux visiteurs distincts, et un scraper qui fait tourner 200 IPs ne peut pas se cacher derrière s’il garde le même cookie.
  • Sépare signaux durs et signaux mous : les premiers déclenchent un ban immédiat, les seconds demandent plusieurs occurrences. Pas de ban d’humain sur un seul indicateur faible.