Mon blog a 3 mois. 50+ articles publiés, un système de commentaires sans base de données, un honeypot anti-spam. Et ce matin, premier commentaire. De "RobertqueRy". Sur un article gRPC en Go. Pour promouvoir un site de jeux en ligne au Bangladesh. Bienvenue sur internet.
Anatomie d'un spam
Voici le commentaire, dans sa version brute, tel qu'il est arrivé dans le fichier JSON :
Players in Bangladesh are increasingly choosing [url][censuré][/url] for online gaming and rewards. The platform provides access to popular games like slots, rummy and aviator with a welcome bonus for new users. Visit the site to download the APK and start playing today.
Le lien est censuré, mais le reste est intact. Analysons les red flags :
- Le pseudo "RobertqueRy" — prénom anglophone suivi d'un suffixe aléatoire. Pattern classique des bots : ils génèrent des identités semi-aléatoires pour éviter les listes noires de pseudos. Ni trop générique ("user1234"), ni trop humain.
- La syntaxe BBCode
[url]...[/url]— révélatrice. Le bot est générique. Il spamme des forums, des blogs, des wikis, des sites vieux de 15 ans. Il ne sait pas sur quoi il poste. Il envoie du BBCode partout en espérant que quelque chose l'interprète. - "Players in Bangladesh" sur un blog tech français — l'article cible traitait de gRPC et du streaming en Go. Le bot n'a pas lu l'article. Il ne lit rien. Il cherche des formulaires.
- Zéro rapport avec le contenu — aucune tentative de paraître humain, aucune phrase d'accroche liée au sujet. Rentabilité maximale, effort zéro.
Ce qui est instructif, c'est que le bot a passé les défenses en place. Voici ce que le JSON stocké ressemble :
{
"id": "fb0c3b72",
"author": "RobertqueRy",
"date": "2026-03-21T15:45:52+01:00",
"content": "Players in Bangladesh are increasingly choosing [url][censuré][/url] for online gaming and rewards...",
"ip_hash": "9b4be10615075cc7"
}
L'IP est hashée (le système est conçu pour être RGPD-friendly), donc je ne peux pas géolocaliser ni bannir l'adresse source directement. Ce qui m'a obligé à réfléchir différemment.
Pourquoi le honeypot ne suffit pas
Le système initial reposait sur un honeypot : un champ website invisible ajouté au formulaire. Les bots stupides remplissent tous les champs — humains et robots différents. Si le champ est rempli, on rejette la soumission silencieusement.
$website = $_POST['website'] ?? '';
if ($website !== '') {
header('Location: /blog/' . $slug . '#comments');
exit;
}
Ça marche sur les bots de 2010. Le problème, c'est qu'en 2026, les bots analysent le CSS. Ils voient display:none, ils voient visibility:hidden, ils voient aria-hidden="true". Ils ne remplissent pas les champs masqués. RobertqueRy a laissé le champ website vide. Test passé.
Les autres couches en place n'ont pas non plus bloqué :
- CSRF token — protège contre les attaques cross-site (un site tiers qui soumet à ta place). Ça ne protège pas contre un bot qui visite le formulaire, récupère le token, et soumet. Ce bot a fait exactement ça.
- Rate limiting (3 posts / 10 min par IP hash) — ne sert à rien si le bot ne poste qu'une fois. RobertqueRy a posté une fois, attendu, et passé à autre chose. Ou il change d'IP entre chaque cible.
Conclusion : les trois premières couches bloquent les attaques triviales. Contre un bot qui simule un comportement humain minimal, elles ne suffisent pas.
La solution — captcha mathématique serveur
J'aurais pu intégrer Google reCAPTCHA. J'aurais pu utiliser hCaptcha. Je ne l'ai pas fait pour les mêmes raisons :
- Dépendance externe — si Google change son API ou ses tarifs, je suis bloqué
- Tracking — ces services identifient les utilisateurs, ce qui pose un problème RGPD réel
- UX — les "cliquez sur tous les feux tricolores" sont pénibles, surtout sur mobile
La solution retenue : un calcul arithmétique simple, généré côté serveur à chaque chargement de page.
Génération côté template
$a = rand(1, 9);
$b = rand(1, 9);
$_SESSION['captcha_answer'] = $a + $b;
<label>Vérification : <?= $a ?> + <?= $b ?> = ?</label>
<input type="number" name="captcha" required>
Vérification côté handler
$captchaAnswer = $_POST['captcha'] ?? '';
$expectedAnswer = $_SESSION['captcha_answer'] ?? null;
if ($expectedAnswer === null || (int)$captchaAnswer !== (int)$expectedAnswer) {
header('Location: /blog/' . $slug . '?comment_error=Vérification incorrecte.#comment-form');
exit;
}
unset($_SESSION['captcha_answer']); // empêche le rejeu
Trois points clés dans cette implémentation :
- Stockage en session, pas côté client — la réponse attendue n'est jamais envoyée au navigateur. Un bot ne peut pas la lire dans le HTML ni dans un cookie.
- Invalidation après usage — le
unsetempêche le replay. Si un bot enregistre la session et retente avec la même réponse, il échoue. - Nombre aléatoire à chaque chargement — pas de réponse fixe à hardcoder. Le bot doit charger la page, parser le HTML, extraire les deux nombres, calculer la somme. C'est faisable mais ça coûte des ressources et rend le spam non rentable.
Le calcul n'est pas rendu en image (donc lisible par les malvoyants et accessible), mais il n'est pas non plus dans un attribut HTML facilement parsable. La friction est suffisante pour décourager les bots généralistes.
Supprimer le spam à distance
Problème immédiat : le commentaire est déjà en ligne. Le fichier blog/comments/grpc-go-streaming-microservices.json est sur le serveur OVH. Ce répertoire est gitignored — je ne peux pas juste pousser un fichier vide via déploiement.
Solution directe : connexion FTP avec lftp, suppression et remplacement par un tableau vide.
lftp -c "open -u user,pass ftp.cluster121.hosting.ovh.net; \
rm /www/blog/comments/grpc-go-streaming-microservices.json; \
put empty.json -o /www/blog/comments/grpc-go-streaming-microservices.json; \
quit"
Où empty.json est un fichier local contenant simplement []. La commande écrase le fichier distant avec un tableau de commentaires vide. Vérification avec un curl sur la page : RobertqueRy a disparu.
C'est basique, mais c'est suffisant. Pour un blog personnel avec des commentaires rares, une interface d'administration dédiée serait du sur-engineering. Si le spam devient fréquent, je reconsidère.
Les 4 couches anti-spam
La stack anti-spam finale, dans l'ordre de filtrage :
- Honeypot — attrape les bots stupides qui remplissent tous les champs du formulaire sans discriminer. Zéro friction pour l'utilisateur humain, efficace contre les outils bas de gamme.
- CSRF token — empêche les soumissions cross-site. Un formulaire posté depuis un autre domaine est rejeté. Protège contre une classe d'attaques différente du spam pur.
- Rate limiting — maximum 3 commentaires par tranche de 10 minutes par IP hashée. Bloque les campagnes de spam en volume depuis une même source.
- Captcha mathématique — bloque les bots qui passent les trois premières couches. Nécessite de charger la page, extraire les opérandes, calculer et soumettre la bonne réponse.
Aucune de ces couches n'est parfaite seule. Ensemble, elles rendent le spam non rentable. C'est le seul objectif réaliste : faire en sorte que spammer ce blog coûte plus en ressources que ça ne rapporte.
Pour les détails d'implémentation du système de commentaires complet, voir commentaires sans base de données en PHP et la mise en place des notifications email par SMTP.
Conclusion
RobertqueRy m'a rendu service. Son spam de jeux en ligne au Bangladesh m'a poussé à ajouter un captcha que j'aurais dû mettre dès le départ. 10 minutes de travail, zéro dépendance externe, et une leçon gratuite : sur internet, si tu as un formulaire public, quelqu'un va essayer de poster des conneries dedans. La seule question c'est quand.
Le captcha mathématique n'est pas invulnérable. Un bot dédié qui parse le HTML pourrait le contourner. Mais pour un blog de développeur indépendant, les bots généralistes sont la menace réaliste. Les bots dédiés ciblent des plateformes avec du trafic, des backlinks qui comptent, un ROI visible. Pas un blog de 3 jours sur le gRPC en Go.
Si RobertqueRy revient, j'aviserai.