Skip to content
← 1001 Widgets
N° 049 · ●●●

Anti-bot form

A contact form that spots bots without a captcha: trap field, time trap, validation and a send limit, like the contact form on this site. The right exercise to understand why real protection happens on the server.

JSserveur
What you'll learn
  • Hide a trap field that only bots fill in
  • Reject submissions too fast for a human
  • Limit submissions per address on the server
Why this language

Pourquoi un formulaire anti-robots ? Mettre un formulaire en ligne, c'est recevoir des dizaines de messages publicitaires par jour. Les captchas agacent les vrais visiteurs. Il existe des pièges plus discrets.

Le champ piège est caché aux humains : un robot qui remplit tout le remplit aussi, et se trahit. Le piège de temps rejette les envois faits en moins de trois secondes : personne n'écrit un message aussi vite. La limite d'envois bloque les rafales venant d'une même adresse.

Toutes ces vérifications doivent tourner sur le serveur : un robot n'exécute pas forcément le JavaScript de la page. La démo les simule dans le navigateur pour qu'on les voie. Et l'adresse qui reçoit les messages reste dans une variable d'environnement, jamais dans la page.

Pourquoi en JavaScript simple ? Côté navigateur, une validation qui aide l'humain. Côté serveur, quelques if qui protègent vraiment.

How it works
  1. LE CHAMP PIÈGE

    Hors de l'écran, sauté au clavier, ignoré des lecteurs d'écran. Seuls les robots le remplissent.

    <div class="piege" aria-hidden="true">
      <label for="site">Ne pas remplir</label>
      <input id="site" name="site" tabindex="-1" autocomplete="off">
    </div>
    <input type="hidden" name="ouvert" value="">   <!-- l'heure d'ouverture, remplie au chargement -->
  2. CÔTÉ SERVEUR : UNE ROUTE NEXT.JS

    app/api/contact/route.ts : c'est ici que tout se décide.

    const envois = new Map();                    // adresse IP vers liste d'horaires
    export async function POST(req) {
      const d = await req.formData();
      const ip = req.headers.get('x-forwarded-for') ?? 'inconnue';
  3. LES PIÈGES

    On répond « ok » au robot : inutile de lui dire ce qui l'a trahi.

      if (d.get('site')) return Response.json({ ok: true });
      if (Date.now() - Number(d.get('ouvert')) < 3000) return Response.json({ ok: true });
  4. LA LIMITE D'ENVOIS

    Trois messages par minute et par adresse, pas plus.

      const recents = (envois.get(ip) ?? []).filter(t => Date.now() - t < 60000);
      if (recents.length >= 3) return Response.json({ erreur: 'Trop d\'envois' }, { status: 429 });
      envois.set(ip, [...recents, Date.now()]);
  5. VALIDER À NOUVEAU, PUIS ENVOYER

    Le navigateur a déjà vérifié, mais on ne fait jamais confiance au client.

      const mail = String(d.get('mail') ?? '');
      if (!/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(mail)) return Response.json({ erreur: 'E-mail invalide' }, { status: 400 });
      await envoyerMail({ a: process.env.CONTACT_TO, de: mail, texte: String(d.get('msg')).slice(0, 2000) });
      return Response.json({ ok: true });
    }
Pitfalls

PIÈGE 1 : Ne vérifier que dans le navigateur

Pitfall

Mauvais : bloquer les robots avec du JavaScript dans la page. Un robot envoie directement la requête au serveur, sans jamais l'exécuter.

Fix

Correct : refaire chaque contrôle sur le serveur. Le navigateur ne sert qu'à aider l'humain à corriger.

PIÈGE 2 : Cacher le piège avec display: none

Pitfall

Mauvais : un champ piège en display: none. Les robots les plus simples savent l'ignorer.

Fix

Correct : le sortir de l'écran en CSS, avec aria-hidden et tabindex="-1" pour ne gêner personne.

PIÈGE 3 : Afficher l'adresse de réception

Pitfall

Mauvais : mettre l'e-mail dans un mailto ou dans le code de la page. Il sera aspiré et inondé de spam.

Fix

Correct : l'adresse vit dans une variable d'environnement du serveur, et le formulaire passe par une route.

PERSONNALISER
  • Un message de confirmation envoyé au visiteur, sans jamais révéler l'adresse de réception.
  • Un filtre de mots interdits, ou un nombre maximal de liens par message.
  • Turnstile de Cloudflare, invisible pour la plupart des visiteurs, si les pièges ne suffisent plus.
OPTIMISATION
  • En production sur plusieurs serveurs, gardez les compteurs dans un stockage partagé comme Redis ou une table de base de données, pas en mémoire.
  • Signez l'heure d'ouverture côté serveur : un robot ne pourra pas la falsifier.
VERS REACT

Avec Next.js, une Server Action peut recevoir le formulaire directement et faire les mêmes vérifications.

L'état du formulaire, les erreurs et le message de succès passent par useActionState.

Les messages d'erreur sont reliés aux champs avec aria-describedby et aria-invalid.

Comments
0/1000

Loading comments…

Audience measurement

With your consent, this site measures its traffic with Google Analytics and the Meta pixel, via Google Tag Manager. Nothing is loaded until you choose, and you can change your mind at any time with the "Cookies" link at the bottom of the page.