.png)
7 façons de stopper le spam de formulaires dans Remix / Node.js
Repérez l'activité des bots, utilisez les API de rate limiting intégrées et bloquez les emails jetables.
Résumer avec l'IA :
Votre page contact ou votre formulaire de newsletter est en place, prêt à accueillir de nouveaux visiteurs.
Vous lancez votre site et, quelques jours plus tard, c'est le drame : 1 000 soumissions de formulaire, toutes issues de la même activité de bot.
Je sais, je ne suis pas assez célèbre pour atteindre ces chiffres 🫠 Laissez-moi jeter un œil à mon dashboard une seconde…

Quel rigolo, ce RobertMow, avec ses 990 SOUMISSIONS
Vous voyez où ça va : sécurisons notre endpoint !
Dans ce tutoriel, on passe en revue 7 astuces et leur implémentation pour empêcher les soumissions de formulaires par des bots.
Marquer les adresses IP des bots
Quand une activité ne peut être que celle d'un bot, on marque les adresses IP en les stockant comme malveillantes. Le mécanisme de stockage dépend de votre stack :
Sur Cloudflare Workers, utilisez KV (datastore clé-valeur)
Sur un autre backend, utilisez la session côté serveur
On verra comment repérer une activité de bot dans les étapes suivantes.
Une fois l'adresse IP marquée, ignorez toute soumission de formulaire :
1234567// prevent bots from re-submitting a formconst KV = context.cloudflare.env.KV;const ipKey = `contact_ip_${clientIP}`;if (await KV.get(ipKey)) { console.warn(`IP ${clientIP} has been flagged as a bot`); return Response.json({ success: true });}Empêcher le contournement de la validation côté client
Si ce n'est pas déjà fait, assurez-vous que votre formulaire est validé côté client.
Et si la valeur envoyée au serveur n'est pas validée, c'est que la validation côté client a été contournée : il faut la traiter comme malveillante.
12345if (!(await schema.validate(data))) { console.warn("Client-side form validation bypassed"); await KV.put(ipKey, "bot", { expirationTtl: 86400 }); // 24 hours cooldown return Response.json({ success: true });}Utiliser un nonce côté client
Pour empêcher les attaques par rejeu, on génère un nonce unique pour chaque soumission de formulaire. Ce nonce est stocké côté backend et comparé au nonce fourni par le client.
12345// generate nonce on contact form page renderconst timestamp = Date.now();const randomPart = Math.random().toString(36).substring(2, 15);const nonce = `${timestamp}.${randomPart}`;await KV.put(nonceKey, "unused")1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253/** * Validates if a nonce is valid and not expired * @param nonce The nonce to validate * @param maxAge Maximum age of the nonce in milliseconds (default: 1 hour) * @returns Boolean indicating if the nonce is still valid */export function isNonceValid(nonce: string, maxAge: number = 3600000): boolean { try { const parts = nonce.split("."); if (parts.length !== 2) return false; const timestamp = parseInt(parts[0], 10); const now = Date.now(); // Check if nonce has expired return now - timestamp <= maxAge; } catch (error) { return false; }}...if (!isNonceValid(nonce)) { console.warn("Expired nonce detected"); return Response.json({ success: false, error: t("contact.response.error.sessionExpired"), });}const nonceKey = `contact_nonce_${nonce}`;const nonceState = await KV.get(nonceKey);if (nonceState === "unused") { await KV.put(nonceKey, "used");} else if (nonceState === "used") { console.warn("Duplicate form submission"); return Response.json( { success: false, error: "The form has already been submitted" }, { status: 409, headers: { "Content-Type": "application/json" } } );} else { console.warn("Invalid nonce"); return Response.json( { success: false, error: "Invalid nonce submitted" }, { status: 409, headers: { "Content-Type": "application/json" } } );}Cloudflare Rate Limiting
On utilise l'API Rate Limiting de Cloudflare pour limiter le nombre de soumissions de formulaires depuis une seule adresse IP. Note 1 : ce n'est pas parfait, car les utilisateurs sur réseaux mobiles partagent souvent la même adresse IP. Note 2 : l'API de rate limiting est encore en bêta ouverte (août 2025).
123456const rateLimiter = context.cloudflare.env.RATE_LIMITER_CONTACT;const clientIP = request.headers.get("CF-Connecting-IP") || "";const rateLimitResult = await rateLimiter.limit({ key: `contact_form_${clientIP}`});if (!rateLimitResult.success) { // Return 429 Too Many Requests}Le rate limiter doit être lié à votre environnement Cloudflare Worker. Ça se fait en général dans votre fichier wrangler.toml :
1234[env.production]kv_namespaces = [ { binding = "RATE_LIMITER_CONTACT", id = "unique-namespace-id" }]Champ honeypot
Un champ caché nommé « website » est inclus dans le formulaire. Il est invisible pour les humains, mais les bots vont probablement le remplir. Si le champ contient une valeur, la soumission est rejetée en silence.
12345678// Check honeypot field - if it contains data, it's likely a botconst honeypotField = data.website?.toString();if (honeypotField) { console.warn("Honeypot field triggered"); await KV.put(ipKey, "bot", { expirationTtl: 86400 }); // 24 hours cooldown // Return success to avoid giving bots feedback, but don't process the form return Response.json({ success: true });}Intégration CAPTCHA (hCaptcha)
Le formulaire inclut un challenge hCaptcha pour vérifier que l'utilisateur est bien humain. On utilise le composant React officiel de @hcaptcha/react-hcaptcha.
123456789101112131415// In the form component<HCaptcha ref={captchaRef} sitekey={process.env.HCAPTCHA_SITE_KEY || "10000000-ffff-ffff-ffff-000000000001"} class="hljs-function">(token) => setCaptchaToken(token)}/>// In the submission handlingconst captchaResult = await validateCaptcha(data.hCaptchaToken.toString());if (!captchaResult.success) { return Response.json({ success: false, error: "CAPTCHA verification failed", });}hCaptcha nécessite une site key et une secret key à configurer comme variables d'environnement dans le fichier .env (en local) et dans votre fichier wrangler.jsonc (sur Cloudflare Workers).
HCAPTCHA_SITE_KEY: votre site key hCaptcha pour l'intégration frontendHCAPTCHA_SECRET_KEY: votre secret key hCaptcha pour la vérification
Validation d'email avec Mailchecker
On utilise la bibliothèque mailchecker pour valider les adresses email et détecter les services d'emails jetables. Elle s'appuie sur une base de plus de 55 000 domaines d'emails jetables.
12345678// Validate email with mailcheckerconst email = data.email?.toString() || "";if (email && !mailchecker.isValid(email)) { return Response.json({ success: false, error: "Please use a valid email address", });}La validation d'email est aussi incluse dans le schéma Yup :
123456789email: yup .string() .email(messages.email.invalid) .required(messages.email.required) .test( "is-valid-email", "Email appears to be invalid or disposable", (value) => (value ? mailchecker.isValid(value) : true) ),Éviter les faux positifs
Je ne recommande pas le filtrage de contenu, par exemple sur des mots-clés (« nigérian », « prince »), car ça peut produire des faux positifs.
En résumé
Si vous êtes pressé, les captchas restent une option correcte : la plupart des bots n'y arrivent pas.
Il suffit de vérifier la valeur du captcha côté serveur, pour qu'on ne puisse pas la contourner.
C'est ce qui a marché pour moi contre le spam, j'espère que ça vous sera utile aussi.
Vous avez déjà subi une activité malveillante, des attaques DDoS ou des formulaires spammés ?
Je suis curieux de savoir comment vous avez réglé ça, et je veux bien entendre les histoires bizarres ou drôles qui vont avec.
Comments (0)
No comments yet. Be the first!