Anti-spam multicouche d'un formulaire de contact
Reference de la chaine de traitement d'un formulaire de contact public soumis a des envois automatises (bots). Le principe directeur est la defense en profondeur : plusieurs couches independantes, chacune activable ou desactivable sans toucher aux autres. Aucune couche n'est un point de defaillance unique ; elles se cumulent.
Cette page decrit la structure et les parametres. Elle ne prescrit pas d'implementation.
1. Ordre de la chaine de traitement
Les controles s'appliquent dans un ordre du moins couteux au plus couteux, afin d'ecarter le trafic evident avant d'engager des ressources (appel reseau, appel d'un modele de langage).
| Rang | Couche | Cout | Etat typique |
|---|---|---|---|
| 0 | Jeton anti-CSRF | nul | pre-existant (framework) |
| 1 | Limitation de debit par IP (rate limiting) | faible | pre-existant |
| 2 | Honeypot (champ piege) | faible | ajoutee |
| 3 | Validation du formulaire (types, contraintes) | faible | pre-existant |
| 4 | CAPTCHA Proof-of-Work | moyen | ajoutee |
| 5 | Validation semantique par LLM | eleve | ajoutee |
| 6 | Envoi effectif (email) | — | pre-existant |
Regle de conception : une requete ne progresse a la couche N+1 que si elle a passe la couche N. L'appel au modele de langage (couche 5, le plus couteux) n'est atteint que par du trafic ayant deja franchi honeypot et CAPTCHA.
2. Couche honeypot (champ piege)
Principe
Un champ de formulaire present dans le DOM mais invisible et non atteignable pour un humain. Un robot qui remplit aveuglement tous les champs le renseigne ; un humain ne le voit jamais. Cote serveur, un champ piege non vide = rejet.
Regles
- Nom de champ credible (ex. un intitule banal qu'un bot serait tente de remplir), pour ne pas signaler le piege.
- Champ non mappe au modele de donnees, non requis.
- Masquage CSS hors-flux (positionnement hors ecran, opacite nulle,
interactions desactivees) — pas
display:none, contournable par certains bots et parfois ignore differemment. - Rejet silencieux : renvoyer au visiteur le meme retour qu'un envoi reussi (leurre). Ne jamais signaler la detection, sous peine d'aider l'attaquant a contourner.
Accessibilite
Le champ ne doit pas perturber les technologies d'assistance :
aria-hidden="true"sur le conteneur ;tabindex="-1"sur le champ (hors parcours clavier) ;- masquage visuel sans
display:none.
Reference : WCAG 1.3.1 (information et relations).
3. Couche CAPTCHA Proof-of-Work
Critere de choix
Privilegier un mecanisme Proof-of-Work auto-hebergeable et open source plutot qu'un service tiers :
- calcul cote navigateur (hashcash / hachage), sans cookie tiers ni tracking ;
- defi genere sur votre propre serveur → aucune dependance reseau externe, aucune donnee utilisateur envoyee a un tiers ;
- composant web natif regroupe avec vos assets locaux (zero CDN externe).
Solutions ecartees et pourquoi :
| Type de solution | Raison d'ecart |
|---|---|
| CAPTCHA de grand acteur publicitaire | donnees envoyees a un tiers, enjeux RGPD |
| Service tiers hebergeant le defi | dependance reseau, compte requis, donnees exfiltrees |
| Solutions non entierement open source | auditabilite limitee |
Un CAPTCHA Proof-of-Work n'oppose pas une enigme visuelle a l'utilisateur : il impose au navigateur un petit cout de calcul (fraction de seconde), transparent pour l'humain mais couteux a l'echelle d'une campagne de spam.
Flux (schematique)
[Client] [Serveur]
| |
|-- GET /defi ------------------------> genere un defi signe (HMAC)
| renvoie {algorithme, defi, sel, signature, ...}
| |
| le composant calcule la preuve |
| (thread de travail, ~0.5 s) |
| |
|-- POST /contact (preuve encodee) ---> verifie la solution + la signature
| invalide -> erreur de formulaire
| valide -> suite de la chaine
Parametres
| Parametre | Role | Note |
|---|---|---|
| Secret HMAC | signe les defis cote serveur | genere aleatoirement (>= 32 caracteres), stocke hors du code source |
| Complexite | nombre d'essais moyen du Proof-of-Work | arbitrage temps navigateur / cout attaquant ; plus haut = plus lent cote client |
| Langue | textes du composant | a forcer dans la langue du site |
L'endpoint de defi est public, sans authentification, et renvoie du JSON.
Accessibilite
- Label textuel explicite (ex. « Je ne suis pas un robot »), pas une icone seule — WCAG 1.1.1, 4.1.3 ;
- navigable au clavier, focus visible — WCAG 2.1.1, 2.4.7 ;
- langue du composant configuree — WCAG 3.1.2.
4. Couche validation semantique par LLM
Principe
Une fois le formulaire valide et les couches honeypot + CAPTCHA franchies, le contenu du message est soumis a un modele de langage pour une analyse semantique anti-spam. Le modele retourne une decision structuree qui conditionne l'envoi ; la decision est journalisee.
Cette couche capture ce que les couches mecaniques ne voient pas : publicite redigee a la main, template generique, langue incoherente avec un vrai besoin, adresse email manifestement jetable.
Contrat de sortie
Le modele doit repondre uniquement en JSON, sans texte autour :
{"spam": true, "reason": "courte explication (max 80 caracteres)"}
Un value object cote serveur porte trois informations : isSpam (booleen),
reason (chaine), et un indicateur apiError (booleen) distinguant une reponse
valide d'une defaillance d'appel.
Prompt systeme (structure recommandee)
Le prompt doit poser explicitement :
- Le role : filtre anti-spam pour un formulaire de contact professionnel, dans la langue cible.
- Le format de sortie : JSON strict, rien d'autre.
- Les criteres SPAM : publicite, SEO, liens suspects, template de bot, langue incoherente, hors-sujet total, email jetable.
- Les criteres NON-SPAM : questions techniques legitimes, demandes de devis, messages courts sinceres, fautes d'orthographe, ton informel.
- La regle de doute : en cas d'incertitude, classer NON-SPAM (fail safe) — on privilegie de ne pas perdre un message legitime.
Parametres d'appel
| Parametre | Valeur type | Justification |
|---|---|---|
| Modele | petit modele rapide et economique de la gamme | tache courte de classification |
| Tokens max de sortie | tres bas (reponse JSON breve) | reponse = un objet compact |
| Timeout | quelques secondes | ne pas bloquer l'UX si le service tarde |
| Cle d'API | injectee via l'environnement | jamais dans le code source |
Le choix du modele est configurable cote administration pour permettre de monter en gamme si le petit modele se revele insuffisant.
Politique fail-open / fail-close
Comportement en cas de defaillance du service (timeout, erreur serveur, JSON
invalide), pilote par un parametre fail_open :
| Situation | fail_open = true (defaut) |
fail_open = false |
|---|---|---|
| Reponse valide, spam detecte | Bloque + message explicite | Bloque + message explicite |
| Reponse valide, non-spam | Laisse passer | Laisse passer |
| Erreur de service | Laisse passer, journalise l'erreur | Bloque + message « verification indisponible » |
Le defaut recommande est fail-open : une panne du filtre IA ne doit pas priver l'utilisateur legitime de la possibilite d'envoyer son message. Les couches honeypot et CAPTCHA restent actives en amont dans tous les cas.
5. Configuration centralisee (administration)
Chaque couche est independante et desactivable depuis un back-office, via un magasin de reglages cle-valeur. Cela permet de couper une couche en cas de faux positifs sans redeploiement.
Cles typiques (noms indicatifs) :
| Cle | Type | Defaut | Role |
|---|---|---|---|
contact.honeypot.enabled |
bool | true |
Active le honeypot |
contact.captcha.enabled |
bool | true |
Active le CAPTCHA Proof-of-Work |
contact.ai_validation.enabled |
bool | true |
Active la validation LLM |
contact.ai_validation.model |
string | (petit modele par defaut) | Modele utilise |
contact.ai_validation.fail_open |
bool | true |
Erreur service -> laisser passer |
contact.message.max_length |
int | 2000 |
Longueur max du message |
Contrainte de longueur du message : une borne trop basse (ex. quelques centaines
de caracteres) est frustrante ; une plage min: 10, max: 2000 (ajustable) evite
les envois vides et les injections massives.
6. Exigences transverses
Journalisation
Un canal de log dedie enregistre chaque evenement de detection :
{nom, email, motif, apiError, ip}. Utile pour ajuster les seuils et detecter
les faux positifs. Ne journaliser que ce qui est necessaire au diagnostic.
Accessibilite des etats progressifs
Si l'UX affiche les etapes de validation en temps reel (CAPTCHA valide, analyse en cours, message envoye) :
- une region
aria-live="polite"presente des le DOM initial (jamais injectee dynamiquement) — WCAG 4.1.3 ; aria-atomic="true"pour lire l'annonce complete ;- badges avec texte alternatif (pas la couleur seule) — WCAG 1.4.1 ;
- bouton d'envoi
disabled+aria-disabled="true"pendant le traitement ; - spinners
role="status"+ label de chargement.
Compteur de caracteres
Champ message avec maxlength HTML et compteur visible relie a une region
aria-live="polite" (le decompte est annonce sans saturer le lecteur d'ecran) —
WCAG 3.3.2.
7. Recapitulatif — pourquoi multicouche
| Couche | Attrape | Cout serveur | Contournable seule |
|---|---|---|---|
| Rate limiting | volume brut par IP | tres faible | oui (IP tournantes) |
| Honeypot | bots naifs | tres faible | oui (bots avances) |
| CAPTCHA PoW | automatisation de masse | client uniquement | oui (fermes humaines) |
| Validation LLM | spam redige, hors-sujet | eleve | oui (contenu parfait) |
Aucune couche n'est suffisante isolement. Leur cumul rend l'attaque economiquement non rentable tout en preservant l'accessibilite et l'experience de l'utilisateur legitime.
Points de vigilance (red-team)
- Le rejet honeypot doit etre indistinguable d'un succes cote client.
- Le secret HMAC du CAPTCHA et la cle d'API du LLM ne vivent jamais dans le code source ni dans le depot — uniquement dans l'environnement.
- Le defaut fail-open protege l'utilisateur mais suppose que les couches amont restent actives ; ne pas desactiver tout en meme temps.
- La validation LLM traite des donnees utilisateur : limiter les tokens, poser un timeout, et ne jamais faire confiance a une sortie non parsee comme JSON strict.