Formulaire de contact sans spam et sans CAPTCHA : 6 protections
Comment arrêter le spam d’un formulaire sans CAPTCHA : piège à robots, ticket horodaté, limites et validation. Du code PHP à copier et l’erreur qui fait envoyer du spam.
Le formulaire de contact est le chemin le plus simple d’un client vers vous, et en même temps la cible préférée des robots. Du spam dans la boîte de réception, ce n’est encore que demi-mal. C’est pire quand un formulaire mal écrit se met lui-même à envoyer du spam depuis votre domaine, et que vos e-mails atterrissent ensuite dans les dossiers « Spam » de vos clients.
La réponse la plus courante au spam est le CAPTCHA : cochez les images avec des bouches d’incendie. Cela fonctionne, mais cela a un coût. Certaines personnes renoncent à envoyer leur message, et reCAPTCHA transmet les données des visiteurs à Google. Dans cet article, je présente 6 protections que le client ne remarque absolument pas. C’est ainsi que fonctionne le formulaire de mon site et ceux des projets que je réalise pour mes clients.
1. Un piège à robots (honeypot)
Les robots remplissent tous les champs qu’ils trouvent dans le code. Vous ajoutez donc un champ qu’un humain ne voit pas. S’il arrive rempli, c’est un robot qui l’a envoyé :
<div class="hp" aria-hidden="true">
<label for="www">Laissez ce champ vide</label>
<input id="www" name="website" type="text" tabindex="-1" autocomplete="off">
</div>
.hp { position: absolute; left: -10000px; width: 1px; height: 1px; overflow: hidden; }
if (trim($_POST['website'] ?? '') !== '') {
// on fait semblant de réussir : le robot ne saura pas qu’il est tombé dans le piège et n’insistera pas
echo json_encode(['ok' => true]);
exit;
}
Trois détails font la différence. Le champ porte un nom banal (« website »), et non « honeypot ». tabindex="-1" fait en sorte qu’une personne qui navigue au clavier n’y accède pas. aria-hidden le masque aux lecteurs d’écran. N’utilisez pas display: none, car certains robots ignorent ces champs.
2. Un ticket horodaté
Un humain a besoin de plusieurs dizaines de secondes pour remplir un formulaire. Un robot l’envoie en une fraction de seconde, souvent sans même charger la page. Au chargement du formulaire, vous émettez donc un ticket avec l’heure et une signature, et à l’envoi vous vérifiez qu’il est authentique et quel âge (en secondes) il a :
const SEKRET = 'longue chaîne aléatoire, par ex. bin2hex(random_bytes(32)), conservée hors du code';
function bilet_nowy(): string
{
$t = (string) time();
return $t . '.' . hash_hmac('sha256', $t, SEKRET);
}
function bilet_ok(string $bilet): bool
{
if (!preg_match('/^(\d{10})\.([a-f0-9]{64})$/', $bilet, $m)) return false;
if (!hash_equals(hash_hmac('sha256', $m[1], SEKRET), $m[2])) return false; // falsifié
$wiek = time() - (int) $m[1];
return $wiek >= 3 && $wiek <= 7200; // pas plus vite qu’en 3 s et pas plus tard qu’après 2 h
}
Vous pouvez insérer le ticket dans le formulaire sous forme de champ caché. Si la page est statique ou mise en cache, récupérez-le par un script depuis une adresse distincte quand quelqu’un commence à remplir le formulaire. Avantage supplémentaire : un robot sans JavaScript ne le recevra jamais. La signature HMAC fait qu’on ne peut pas apprendre au robot à « saisir une heure datant d’il y a une minute », car sans le secret il ne peut pas générer une signature valide.
3. Des limites de soumissions
Même un robot malin a un temps limité. Une limite par connexion, par exemple 5 messages en 10 minutes, et une limite quotidienne globale protègent la boîte de réception contre l’inondation :
function limit_ok(string $ip, int $max = 5, int $okno = 600): bool
{
// hash de l’IP plutôt que l’adresse brute : vous ne conservez pas de données personnelles
$plik = sys_get_temp_dir() . '/formularz-' . hash('sha256', $ip . SEKRET) . '.json';
$fh = fopen($plik, 'c+');
flock($fh, LOCK_EX); // deux envois simultanés n’écraseront pas le compteur
$teraz = time();
$czasy = array_filter(json_decode(stream_get_contents($fh), true) ?: [], fn($t) => $t > $teraz - $okno);
$ok = count($czasy) < $max;
if ($ok) $czasy[] = $teraz;
ftruncate($fh, 0);
rewind($fh);
fwrite($fh, json_encode(array_values($czasy)));
flock($fh, LOCK_UN);
fclose($fh);
return $ok;
}
if (!limit_ok($_SERVER['REMOTE_ADDR'] ?? '')) {
http_response_code(429);
exit;
}
Si le site se trouve derrière un serveur intermédiaire (Cloudflare, Caddy, Nginx), REMOTE_ADDR est l’adresse de ce serveur, et non celle du client. Il faut alors prendre la vraie adresse dans un en-tête défini par votre serveur, mais seulement si vous faites confiance à celui qui le définit. Sinon, un robot s’attribuera n’importe quelle adresse et contournera la limite.
4. Validation de l’e-mail : le piège par lequel un formulaire envoie du spam
C’est le point le plus important de cet article. Un formulaire PHP typique vous envoie le message et définit l’en-tête Reply-To sur l’adresse du client, afin que vous puissiez répondre en un clic. L’adresse est généralement vérifiée ainsi :
filter_var($email, FILTER_VALIDATE_EMAIL)
Le problème, c’est que, selon la norme, une adresse e-mail peut avoir la partie avant @ entre guillemets, et y contenir des caractères précédés d’un antislash, y compris un saut de ligne. Je l’ai vérifié sous PHP 8.3 et 8.4 : une adresse entre guillemets avec un antislash et un saut de ligne à l’intérieur passe cette validation. Insérée dans l’en-tête Reply-To, elle ajoute un nouvel en-tête, par exemple Bcc: avec une liste d’adresses étrangères. Votre formulaire envoie alors du spam depuis votre domaine, via votre serveur de messagerie.
La solution est simple. Pour le formulaire, les adresses ordinaires suffisent : rejetez donc les guillemets, les antislashs et les caractères de contrôle :
function email_ok(string $e): bool
{
return strlen($e) <= 120
&& filter_var($e, FILTER_VALIDATE_EMAIL) !== false
&& !preg_match('/[\x00-\x20\x7F"\\\\(),:;<>\[\]]/', $e);
}
Seconde règle : n’insérez dans les en-têtes de l’e-mail rien qui vienne de l’utilisateur, hormis l’adresse ainsi vérifiée. Pour le sujet et le nom, le mieux est de les encoder (=?UTF-8?B?...?=) ou d’utiliser une bibliothèque d’envoi de courrier, par exemple PHPMailer ou Symfony Mailer, qui surveillent elles-mêmes les en-têtes. J’ai trouvé et corrigé cette erreur lors de l’audit de sécurité de mon propre site ; je sais donc qu’elle concerne du vrai code, et pas seulement les manuels.
5. Le contenu et le domaine de l’expéditeur
Le spam a ses habitudes. Quelques règles simples écartent une bonne partie de ce qui passe à travers les pièges :
$linki = static fn(string $s): int => preg_match_all('~https?://|www\.~i', $s);
if ($linki($imie) > 0 || $linki($tresc) > 3) {
odrzuc('linki'); // un lien dans le nom ou plus de 3 liens dans le contenu
}
// le domaine de l’adresse e-mail doit accepter le courrier (enregistrement MX ou A)
$domena = substr(strrchr($email, '@') ?: '', 1);
if ($domena === '' || (!checkdnsrr($domena, 'MX') && !checkdnsrr($domena, 'A'))) {
odrzuc('domena');
}
La vérification du domaine a un autre avantage : elle repère les fautes de frappe de vrais clients, par exemple gmial.com. Plutôt que de rejeter en silence, demandez alors de corriger l’adresse, sinon votre réponse n’arrivera jamais.
6. Une tâche de calcul pour les formulaires coûteux
Si l’envoi du formulaire coûte quelque chose, par exemple lance une IA, une API payante ou un gros rapport, il vaut la peine d’ajouter une tâche de calcul (proof-of-work). Le navigateur cherche en arrière-plan un nombre qui, avec la chaîne reçue, donne une empreinte SHA-256 commençant par plus d’une dizaine de bits à zéro. Pour un humain, cela prend une fraction de seconde en arrière-plan, tandis qu’un robot qui veut envoyer mille soumissions doit payer mille fois en temps processeur. C’est ainsi que je protège les outils de mon site, comme Analyser votre site et l’audit de la concurrence. Pour un formulaire de contact ordinaire, ce n’est généralement pas nécessaire.
Ce qu’il ne faut pas faire
- Ne dites pas au robot pourquoi il a été rejeté. Une réponse « le champ website doit être vide » est une instruction pour contourner le piège.
- N’enregistrez pas le contenu des messages rejetés. Des compteurs suffisent : combien de rejets et pour quelle raison. Moins de données, c’est moins d’obligations au titre du RGPD.
- Ne bloquez pas à l’aveugle des pays ou des réseaux entiers. Un client en vacances ou dans une entreprise avec un VPN veut aussi pouvoir vous écrire.
- N’oubliez pas les messages destinés aux humains. Quand la limite touche un vrai client, affichez un numéro de téléphone ou une adresse e-mail, et pas seulement un code d’erreur.
Quand recourir tout de même à un CAPTCHA ?
Si malgré ces protections le spam continue d’arriver, cela signifie généralement un humain, ou un robot qui pilote un vrai navigateur. Le CAPTCHA a alors du sens. Choisissez une solution invisible pour la plupart des gens et axée sur la vie privée, décrivez-la dans la politique de confidentialité et n’activez-la que là où le problème existe vraiment.
En résumé
Le piège à robots, le ticket horodaté et les limites arrêtent le spam automatique sans aucun CAPTCHA. Une validation correcte de l’e-mail évite que le formulaire ne se transforme en machine à envoyer du spam depuis votre domaine. Si les e-mails de votre entreprise atterrissent de toute façon en spam chez vos clients, vérifiez aussi les réglages du domaine, que j’explique dans l’article SPF, DKIM et DMARC pas à pas.
Vous voulez que je vérifie les formulaires et la sécurité de votre site ? Commandez un audit de sécurité ou écrivez-moi. Je réponds sous 24 heures, et tout nouveau client bénéficie de 20 % de remise sur sa première commande.
Questions fréquentes
Il arrête les robots les plus simples, qui remplissent tous les champs. Les plus malins le contournent ; il vaut donc la peine d’ajouter un ticket horodaté et des limites. Ensemble, ces trois couches arrêtent l’écrasante majorité du spam automatique, sans aucun CAPTCHA.
reCAPTCHA transmet à Google des données sur le visiteur ; il faut donc le décrire dans la politique de confidentialité, et sa base juridique dans l’UE soulève des doutes. Si un CAPTCHA est vraiment nécessaire, envisagez des solutions axées sur la vie privée, comme Cloudflare Turnstile ou Friendly Captcha, et décrivez-les aussi dans la politique.
Aucun piège n’arrête un humain. Ce qui aide : des limites de soumissions par connexion, le rejet des messages contenant de nombreux liens et la vérification que le domaine de l’adresse e-mail accepte bien du courrier. Le reste, vous le supprimez simplement, mais il y en a peu.
La logique est la même. Les extensions de formulaires courantes ont des pièges à robots et des limites, souvent dans des modules complémentaires. Vérifiez seulement que l’extension anti-spam n’envoie pas le contenu des messages à un service externe, car vous devriez alors aussi le décrire dans la politique de confidentialité.
Services liés au sujet
Vous avez un projet similaire ?
Décrivez votre besoin et je vous envoie un devis sous 24 heures. Les nouveaux clients bénéficient de 20 % de réduction sur la première commande.


