5 min

Protéger un formulaire du spam sans Captcha : d’Akismet à mon anti-spam maison

En novembre 2016, je publiais sur mon blog WordPress un billet intitulé « Comment paramétrer Akismet ? ». J’y expliquais comment activer le module anti-spam livré avec WordPress, récupérer une clé API sur le site officiel, puis choisir entre la suppression directe des commentaires indésirables et une boîte de probation de quatorze jours. Je partageais même un petit filtre à coller dans functions.php pour porter ce délai à trente jours, avec cette phrase qui résume déjà tout : « nous ne sommes jamais à l’abri d’un faux-positif ». Dix ans plus tard, je ne protège plus des commentaires de blog mais les formulaires de mon propre site, construit sur Pulsar, mon framework PHP. Et ce réflexe de 2016, préférer garder un message douteux plutôt que d’en perdre un bon, est devenu la règle centrale de l’anti-spam que j’ai fini par écrire moi-même.

Ce qu’Akismet m’avait appris

À l’époque, je déléguais tout. Akismet combinait un dictionnaire de mots-clés indésirables, des règles et une liste noire, et classait les commentaires en attente ou en indésirable. Pour un blog, c’était efficace et je le recommandais sans réserve.

Mais déléguer a un prix. Chaque message part vers des serveurs tiers pour analyse, ce qui, sur un formulaire de contact où un prospect laisse son nom et son email, est devenu un vrai sujet RGPD. Et surtout, le coût d’un faux positif a changé d’échelle : un commentaire de blog perdu, c’est dommage. Une demande de devis perdue, c’est un client qui ne reviendra pas. Mon ancien site a fini par crouler sous le spam de formulaire de contact, et c’est en repensant le nouveau que j’ai posé une contrainte de départ : filtrer sérieusement, sans jamais faire payer la facture aux humains.

Pourquoi pas de Captcha visible

La réponse habituelle au spam, c’est un Captcha. Je l’ai écartée pour trois raisons.

La friction, d’abord. Chaque étape ajoutée avant l’envoi fait abandonner des visiteurs réels, pendant que les robots sérieux résolvent ces puzzles ou les font résoudre par des fermes de clics. On perd des humains pour freiner des machines qui ne sont plus freinées.

L’accessibilité, ensuite. Identifier des feux de signalisation dans des vignettes floues exclut les personnes malvoyantes, et les alternatives audio sont pénibles pour tout le monde.

La vie privée, enfin. Les Captcha des grandes plateformes chargent des scripts tiers, observent le comportement du visiteur et posent des questions de consentement que je ne voulais pas avoir à gérer. Mon site n’appelle aucun service externe pour ses formulaires : tout est auto-hébergé, rien ne sort.

Le Captcha punit l’humain pour un problème créé par des robots. Je voulais l’inverse : des vérifications que seuls les robots remarquent.

Sept couches invisibles plutôt qu’un mur

Aucune vérification n’est parfaite seule. Empilées, elles arrêtent l’essentiel du spam automatisé sans que le visiteur ne voie quoi que ce soit. Depuis la première version de cet article, j’ai changé d’avis sur un point : je restais au niveau du concept, seuils cachés, par prudence. Un filtre qui ne tient que par le secret de ses paramètres est un filtre fragile ; celui-ci tient même une fois décrit en détail. Le voici donc en entier, chiffres compris. Chaque soumission traverse sept couches, dans cet ordre :

  1. le jeton CSRF, re-vérifié à la soumission ;
  2. la limitation de débit par adresse IP ;
  3. le piège temporel signé ;
  4. le pot de miel ;
  5. le challenge invisible de preuve de travail ;
  6. le score de contenu, qui ne bloque jamais ;
  7. le consentement RGPD explicite.

La porte d’entrée : jeton CSRF et limitation de débit

Avant même de parler de spam, le jeton CSRF émis au rendu du formulaire est re-vérifié à la soumission. C’est une protection classique contre les requêtes forgées, mais elle écarte déjà les scripts qui postent directement vers le point d’envoi sans jamais avoir chargé la page.

Vient ensuite la limitation de débit par adresse IP : dix tentatives par heure et par formulaire, sur une fenêtre fixe, comptées dans le cache fichiers que se partagent les workers PHP. Au-delà, le serveur répond 429 avec un message localisé dans la langue du visiteur. C’est la seule couche qui refuse frontalement : une rafale ne ressemble à aucun humain, même pressé.

Les pièges silencieux : piège temporel et pot de miel

Le piège temporel. Au rendu du formulaire, le serveur émet un champ caché horodaté et signé en HMAC. Un humain met des secondes, souvent des minutes, à lire et remplir un formulaire ; une soumission qui revient en moins de trois secondes environ est un robot. Le site lui répond alors « message envoyé » sans rien envoyer. Et si la signature est invalide ou absente, la couche laisse passer : fail-open, jamais de faux positif. On ne bloque que sur preuve positive d’automatisation, les autres couches couvrent le reste.

Le pot de miel. Un champ caché, invisible pour les humains, que les robots remplissent parce qu’ils remplissent tout. Rempli, même traitement : acceptation silencieuse, aucun envoi. Le robot ne sait pas qu’il a été détecté, donc il n’apprend rien.

Son nom, « ref_code », ne veut rien dire, et c’est délibéré. Chrome et les gestionnaires de mots de passe ignorent largement autocomplete="off" et déduisent le type d’un champ de son libellé. Un piège nommé « site web de l’entreprise » finit rempli par le navigateur d’un vrai visiteur, dont la demande part alors à la poubelle sans que personne le sache. Oui, je publie le nom du champ : un spammeur qui lirait cet article pour l’esquiver tomberait sur les six autres couches.

Le challenge invisible, un Turnstile maison

Restait le robot outillé qui exécute JavaScript et remplit proprement. Pour lui, un défi de preuve de travail auto-hébergé, l’équivalent maison de Turnstile : le serveur émet un jeton signé à usage unique, et le navigateur résout en arrière-plan un petit calcul d’environ seize bits de difficulté pendant que le visiteur écrit son message. Aucun clic, aucune case, aucune image de passage piéton. Imperceptible pour un humain, coûteux pour un robot qui poste en masse, sans clé d’API ni service tiers. Et si le navigateur n’exécute pas JavaScript, un fallback est prévu : l’absence de réponse ne bloque pas l’envoi, les couches côté serveur prennent le relais. La dégradation gracieuse s’applique aussi à la sécurité.

La règle du zéro lead perdu

Sixième couche, le score de contenu : densité de liens, qualité du texte, détection de doublons. Chaque signal ajoute des points à un score, et c’est ici que la philosophie diverge des filtres classiques : ce score ne rejette jamais un envoi. Quand les signaux s’accumulent, le message est livré quand même, simplement marqué « à vérifier » dans ma boîte.

Le pipeline distingue donc deux familles de vérifications. Les preuves positives d’automatisation, comme le pot de miel, le piège temporel ou la rafale, rejettent. Les signaux de contenu, eux, ne font que scorer. Un vrai client qui colle trois liens vers son site existant, ou qui écrit deux lignes sèches depuis son téléphone, passe. Je préfère lire deux spams que perdre un client. C’est exactement la logique de mes trente jours de probation Akismet en 2016, poussée jusqu’au bout : le doute profite toujours au message.

La septième couche n’a rien d’algorithmique : une case de consentement RGPD explicite, la seule partie de tout ce pipeline que le visiteur voit. Après avoir retiré les scripts tiers justement pour la vie privée, autant être irréprochable jusque dans le formulaire lui-même.

Le tableau de bord honnête

Un anti-spam se juge sur pièces, pas sur promesses. Voici les quatre scénarios que j’ai vérifiés en conditions réelles, sans en cacher aucun :

  • un client réel remplit le formulaire : le message est délivré ;
  • un robot simple remplit tout, y compris le pot de miel : accepté en silence, jamais envoyé ;
  • un robot rapide soumet en moins de trois secondes : le piège temporel le repère, accepté en silence, jamais envoyé ;
  • une rafale depuis la même adresse : 429 après la dixième tentative dans l’heure.

Ce que ça donne en pratique

Sur la page contact comme sur le calculateur de devis, le visiteur ne voit rien de cette mécanique. Pas de case « je ne suis pas un robot », pas de puzzle, pas de bannière de consentement pour un script tiers. Le formulaire se remplit et s’envoie, point. C’est le même soin que je mets dans l’ensemble de mon travail de développeur web en Belgique : la robustesse ne doit jamais se payer en friction pour l’utilisateur.

Si votre propre formulaire croule sous le spam, ma suggestion tient en une phrase : avant d’ajouter un Captcha qui fera fuir vos visiteurs, empilez des vérifications invisibles et réservez le rejet aux preuves d’automatisation. En 2016, je terminais mon billet en vous invitant à commenter et partager. En 2026, la version honnête est plus simple : si le sujet vous parle, écrivez-moi via ce fameux formulaire. Il tiendra le choc.

Lenny Obez

Ingénieur logiciel à Huy. Je construis des plateformes qui tiennent les jours où tout se joue.

Envie de collaborer ?