Arrêter le spam des formulaires de contact

Arrêter le spam des formulaires de contact7 min de lecture

Comment limiter le débit des envois de formulaire

Limitez les envois de formulaire par visiteur avec un stockage partagé, répondez 429 avec Retry-After, et évitez de bloquer les bureaux qui partagent une IP.

Preet SutharFondateur, Ikiform

Comptez les envois par IP de visiteur dans un stockage que partagent toutes les instances du serveur, comme Redis, autorisez une petite rafale, et répondez 429 avec un en-tête Retry-After quand le compteur est épuisé. Un compteur en mémoire ne fonctionne que sur un seul serveur qui tourne en continu, jamais sur les hébergeurs serverless. Fixez la limite d'après l'usage réel du formulaire, et laissez de la marge pour les bureaux et les événements où beaucoup de personnes partagent une IP.

Ce qui distingue les limites de débit

Une limite de débit ne distingue pas le spam du courrier légitime. Elle arrête le volume : un script qui envoie en boucle, un bot qui passe en revue votre formulaire, ou une personne qui martèle le bouton. C'est donc la couche la moins coûteuse, puisqu'elle s'exécute avant la lecture du corps, et la plus grossière, puisqu'elle ne distingue pas un attaquant d'un bureau animé. Trois choses décident si elle aide ou nuit : où vit le compteur, ce par quoi vous comptez, et la vitesse à laquelle le quota revient.

Une rafale d'envois d'un visiteur face à une limite par visiteur à recharge progressiveenvois 1 à3en 10secondes :acceptésenvoi 4429 avecRetry-Aftertoutes les12 sun envoi deplus autoriséune minutecalmequota completà nouveaurafaleattenterechargeUne rafale d'envois d'un visiteur face à une limite par visiteur à recharge progressiveenvois 1 à 3en 10 secondes : acceptésenvoi 4429 avec Retry-Aftertoutes les 12 sun envoi de plus autoriséune minute calmequota complet à nouveaurafaleattenterecharge
  1. Un visiteur envoie 3 fois en 10 secondes. Tous les envois sont acceptés.
  2. Le suivant est refusé avec 429 et un en-tête Retry-After qui indique combien de temps attendre.
  3. Ensuite, un envoi de plus est autorisé toutes les 12 secondes.
  4. Après une minute calme complète, tout le quota est revenu.
Ce sont les chiffres Standard d'Ikiform : 5 par minute, au plus 3 toutes les 10 secondes. Une recharge progressive évite d'attendre toute une fenêtre pour un envoi de plus.

Où vit le compteur

StockageFonctionne surPoints de vigilance
Une Map en mémoireUn seul serveur qui tourne en continuPerdu au redémarrage. Chaque instance serverless a sa propre map : la limite réelle est donc inconnue
Redis, comme UpstashTout, y compris serverless et edgeUn appel réseau par envoi. Choisissez une région proche de votre serveur
Binding de limitation de débit de Cloudflare WorkersWorkersApproximatif et compté par emplacement Cloudflare, avec des périodes de 10 ou 60 secondes uniquement
Un reverse proxy, comme limit_req de nginxServeurs derrière votre propre proxyCompte par proxy. Nécessite la vraie IP du client

L'algorithme compte moins que le stockage, mais il façonne ce que ressentent les visiteurs. Une fenêtre fixe se réinitialise à chaque minute : un script peut donc envoyer un quota complet à la fin d'une minute et un autre au début de la suivante. Une fenêtre glissante lisse ce bord en pondérant la fenêtre précédente. Un token bucket, ou l'algorithme similaire qu'utilise Ikiform, recharge un envoi à la fois, ce qui permet à une personne qui a fait une faute de frappe de renvoyer quelques secondes plus tard sans ouvrir la porte à une rafale. Pour un formulaire de contact, n'importe lequel convient si les chiffres correspondent à l'usage réel.

Les exemples ci-dessous utilisent @upstash/ratelimit d'Upstash, car il fonctionne de la même façon dans chaque framework et sur les hébergeurs serverless. Définissez UPSTASH_REDIS_REST_URL et UPSTASH_REDIS_REST_TOKEN, et enregistrez le helper une fois :

lib/limit.ts
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";

const contact = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(5, "60 s"),
  prefix: "contact",
});

export async function allow(ip: string | null | undefined) {
  const { success, reset } = await contact.limit(ip || "no-ip");
  return { ok: success, retryAfter: Math.max(1, Math.ceil((reset - Date.now()) / 1000)) };
}

reset est un temps Unix en millisecondes : le helper le convertit donc en secondes entières pour Retry-After. Les envois sans IP partagent un même compteur, ce qui empêche un en-tête manquant de devenir un laissez-passer.

L'appliquer dans votre framework

Un formulaire HTML simple n'a pas de serveur pour compter quoi que ce soit. Envoyez-le à Ikiform et choisissez un préréglage sur le formulaire ; un visiteur au-delà de la limite voit une courte page de ralentissement au lieu d'une erreur.

contact.html
<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="POST">
  <label for="email">Email</label>
  <input id="email" name="email" type="email" required />
  <label for="message">Message</label>
  <textarea id="message" name="message" required></textarea>
  <button>Send</button>
</form>

Sur Cloudflare Workers, le binding intégré est l'option la moins coûteuse :

wrangler.toml
[[ratelimits]]
name = "CONTACT_LIMIT"
namespace_id = "1001"

  [ratelimits.simple]
  limit = 5
  period = 60
src/index.ts
const ip = request.headers.get("CF-Connecting-IP") ?? "no-ip";
const { success } = await env.CONTACT_LIMIT.limit({ key: `contact:${ip}` });
if (!success) return new Response("Too many messages", { status: 429, headers: { "Retry-After": "60" } });

Cloudflare documente ce binding comme approximatif et compté séparément dans chaque emplacement : une attaque répartie peut donc le dépasser. Pour une limite exacte sur tous les emplacements, Cloudflare recommande un Durable Object. Derrière votre propre nginx, limit_req fait le comptage avant que votre application ne voie l'envoi :

nginx.conf
limit_req_zone $binary_remote_addr zone=contact:10m rate=5r/m;

server {
  location = /api/contact {
    limit_req zone=contact burst=3 nodelay;
    limit_req_status 429;
    proxy_pass http://127.0.0.1:3000;
  }
}

Par quoi compter

  • IP et formulaire ensemble. Le choix habituel. Un visiteur ne peut pas inonder ce formulaire, et utiliser un autre formulaire de votre site ne consomme pas le même quota.
  • Beaucoup de personnes, une IP. Les bureaux, écoles, opérateurs mobiles et le Wi-Fi des conférences placent beaucoup de personnes derrière une même adresse. Gardez la limite par IP assez souple pour elles, ou relevez-la les jours d'événement.
  • La vraie IP du client. Derrière un proxy ou un CDN, lisez l'en-tête que définit votre hébergeur et ne faites confiance qu'à celui-là. Une valeur brute de X-Forwarded-For peut être définie par le client.
  • L'IP de votre propre serveur. Si votre serveur transmet les envois à un autre service, tous les visiteurs y partagent l'IP de votre serveur. Limitez de votre côté, par IP du visiteur, avant de transmettre.

Les limites de débit d'Ikiform

Ikiform compte chaque envoi précisément, par IP de visiteur et par formulaire, avant de lire le corps, avec une recharge progressive plutôt que des fenêtres fixes. Chaque formulaire choisit un préréglage sous Rate limit. Une protection anti-flot de 600 par minute par IP sur tous les formulaires s'exécute avant chacun d'eux.

PréréglagePar visiteurConvient à
Standard5 par minute, au plus 3 toutes les 10 secondesLa plupart des formulaires
Relaxed20 par minuteÉvénements sur un Wi-Fi partagé, formulaires derrière une connexion
Strict2 toutes les 10 minutes, un de plus toutes les 5 minutesFormulaires de contact et commerciaux
Custom1 à 1 000 par 10 secondes, minute, 10 minutes, heure ou jourUne inscription par personne, bornes

Le formulaire entier accepte aussi 300 envois par minute pour l'ensemble des visiteurs, sauf si vous définissez un plafond Custom. Au-delà de la limite, un envoi du navigateur voit une page de ralentissement et fetch reçoit 429 rate_limited avec Retry-After, qu'Ikiform expose via CORS pour que le code ci-dessus puisse le lire. La documentation sur les limites de débit explique la recharge.

Les versions côté serveur envoient avec une clé secrète, donc Ikiform ignore Allowed domains et sa vérification humaine. Chaque envoi vient alors de l'IP de votre serveur, et la limite Standard de 5 par minute et par visiteur compte tous vos visiteurs ensemble. Choisissez la limite de débit Relaxed ou Custom pour un formulaire que vous envoyez via un serveur, et gardez IKIFORM_SECRET_KEY dans des variables d'environnement réservées au serveur.

Vérifiez que tout fonctionne

  1. Envoyez le formulaire plusieurs fois de suite. Au-delà de la limite, vous recevez 429 avec un en-tête Retry-After, ou la page de ralentissement d'Ikiform.
  2. Attendez le nombre de secondes indiqué dans Retry-After et renvoyez. L'envoi est accepté.
  3. Envoyez depuis un second réseau, par exemple votre téléphone hors Wi-Fi. Il a son propre quota.
  4. Si votre serveur transmet les envois, envoyez rapidement depuis deux appareils et vérifiez qu'aucun ne partage le compteur de l'autre.

Pièges

  • Les envois refusés ne doivent pas consommer de quota. Comptez avant le travail, et ne laissez pas un envoi refusé repousser la fenêtre, sinon un visiteur nerveux se bloque lui-même.
  • Ne limitez pas le chargement de la page. Comptez les envois, pas les affichages du formulaire, sinon une personne qui recharge épuise ses envois.
  • 429 rate_limited Un visiteur a envoyé plus que ce que permet la limite de débit du formulaire. Standard autorise 5 envois par minute par visiteur. Attendez Retry-After, ou choisissez Relaxed ou Custom pour les formulaires envoyés depuis une IP partagée.
  • IPv6. Une personne peut détenir de nombreuses adresses IPv6. Pour une limite stricte, comptez par préfixe /64 plutôt que par adresse complète.

Questions

Une étape vous bloque, ou vous voulez un guide pour votre cas ? Écrivez à hello@ikiform.com.