# Comment arrêter le spam des formulaires sans captcha

> Arrêtez l'essentiel du spam sans captcha : un honeypot, une heure de rendu signée, un score du contenu et une limite de débit, cumulés plutôt qu'utilisés seuls.

Publié le 2026-10-25 · https://ikiform.com/fr/guides/spam-protection/without-captcha

Combinez quatre vérifications invisibles et additionnez ce qu'elles trouvent : un honeypot, un horodatage signé qui montre combien de temps le formulaire est resté ouvert, un score pour les liens et les formules de spam, et une limite de débit par visiteur. Ne traitez un envoi comme du spam que si le total dépasse un seuil : aucun signal faible ne bloque ainsi à lui seul une vraie personne. Conservez ce que vous attrapez, et n'ajoutez un captcha plus tard que si le dossier de spam montre que des bots passent encore.

## Ce qui change sans captcha

Un captcha vous donne un oui ou un non franc. Sans lui, vous avez plutôt plusieurs signaux faibles, chacun facile à contourner ou à déclencher par accident. Un envoi rapide peut venir d'un bot, ou de quelqu'un qui a collé un message préparé. Un lien peut être du spam, ou un portfolio. L'astuce est de noter : donnez un poids à chaque signal, additionnez, et ne parlez de spam qu'au-delà d'un seuil. Le seul signal fort, un honeypot rempli, reçoit un poids assez grand pour franchir la ligne à lui seul.

Schéma : Comment des signaux invisibles s'additionnent en un verdict pour trois envois différents

1. Un bot scripté remplit le honeypot, ce qui ajoute 5, et envoie en moins d'une seconde, ce qui ajoute 3. Son total de 8 en fait du spam.
2. Un démarchage SEO collé a un lien dans le champ du nom, 3, et une formule de spam connue, 2. Son total de 5 atteint le seuil de 5.
3. Un vrai message avec trois liens obtient 2 et rien d'autre : il est donc reçu.

Les poids sont ceux d'Ikiform, avec un seuil de 5. Deux signaux faibles ensemble le franchissent ; un seul, jamais.

## Une heure de rendu signée

Il faut plusieurs secondes à une personne pour remplir un formulaire. Un piège temporel mesure cet écart. La version faible place `Date.now()` dans un champ masqué, que n'importe quel bot peut réécrire. La version solide signe l'heure de rendu sur le serveur avec un secret : un bot ne peut donc pas prétendre que la page a été ouverte plus tôt. Ce fichier tourne partout où il y a Web Crypto : Node 20 et plus, Deno, Bun, Workers et tous les runtimes serverless.

`lib/render-token.ts`:

```ts
const encoder = new TextEncoder();

async function key(secret: string) {
  return crypto.subtle.importKey("raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, [
    "sign",
    "verify",
  ]);
}

function toHex(bytes: ArrayBuffer) {
  return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
}

function fromHex(hex: string) {
  return new Uint8Array((hex.match(/../g) ?? []).map((pair) => Number.parseInt(pair, 16)));
}

export async function signRender(secret: string, now = Date.now()) {
  const signature = await crypto.subtle.sign("HMAC", await key(secret), encoder.encode(`render.${now}`));
  return `${now}.${toHex(signature)}`;
}

export type RenderCheck = "ok" | "too_fast" | "invalid";

export async function checkRender(secret: string, token: string, minSeconds = 3, now = Date.now()): Promise<RenderCheck> {
  const [time = "", signature = ""] = token.split(".");
  if (!/^\d{13}$/.test(time) || !/^[0-9a-f]{64}$/.test(signature)) return "invalid";
  const valid = await crypto.subtle.verify("HMAC", await key(secret), fromHex(signature), encoder.encode(`render.${time}`));
  const age = now - Number(time);
  if (!valid || age < 0 || age > 86_400_000) return "invalid";
  return age < minSeconds * 1000 ? "too_fast" : "ok";
}
```

Signez au rendu de la page : la page doit donc être rendue à chaque requête, pas mise en cache en HTML statique. Dans Astro :

`src/pages/contact.astro`:

```astro
---
import { signRender } from "../lib/render-token";

export const prerender = false;

const rendered = await signRender(import.meta.env.RENDER_SECRET);
---

<form method="POST" action="/api/contact">
  <input type="hidden" name="rendered" value={rendered} />
  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
  <label for="name">Name</label>
  <input id="name" name="name" required />
  <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>
```

- Trois secondes est un minimum sûr pour un formulaire de contact. Quelqu'un qui colle un message préparé peut quand même en mettre deux.
- Refusez les tokens de plus d'un jour. Sinon, un bot charge la page une fois et réutilise le même token pendant des semaines.
- Traitez un token absent ou falsifié comme un signal, pas comme une preuve. Une page en cache ou un vieil onglet peut en produire un.

## Noter l'envoi

Réunissez les signaux dans une seule fonction. Les vérifications de liens et de formules attrapent ce que le honeypot et le délai ne peuvent pas : les messages tapés ou collés par des personnes. Gardez la liste de formules courte et précise ; des mots larges comme `free` signalent de vrais clients.

`lib/score.ts`:

```ts
const WEIGHTS = { honeypot: 5, too_fast: 3, invalid_render: 3, link_in_name: 3, many_links: 2, phrase: 2 };
const THRESHOLD = 5;
const LINK = /https?:\/\/|www\./gi;
const PHRASES = /\b(seo services|guest post|backlinks|first page of google|crypto investment)\b/i;

type Reason = keyof typeof WEIGHTS;

export function scoreContact(input: {
  name: string;
  message: string;
  trap: string;
  render: "ok" | "too_fast" | "invalid";
}) {
  const reasons: Reason[] = [];
  if (input.trap.trim()) reasons.push("honeypot");
  if (input.render === "too_fast") reasons.push("too_fast");
  if (input.render === "invalid") reasons.push("invalid_render");
  if (input.name.match(LINK)) reasons.push("link_in_name");
  if ((`${input.name} ${input.message}`.match(LINK)?.length ?? 0) >= 3) reasons.push("many_links");
  if (PHRASES.test(`${input.name} ${input.message}`)) reasons.push("phrase");
  const score = reasons.reduce((total, reason) => total + WEIGHTS[reason], 0);
  return { score, reasons, spam: score >= THRESHOLD };
}
```

`src/pages/api/contact.ts`:

```ts
import type { APIRoute } from "astro";
import { checkRender } from "../../lib/render-token";
import { scoreContact } from "../../lib/score";

export const POST: APIRoute = async ({ request, redirect }) => {
  const form = await request.formData();
  const render = await checkRender(import.meta.env.RENDER_SECRET, String(form.get("rendered") ?? ""));
  const verdict = scoreContact({
    name: String(form.get("name") ?? ""),
    message: String(form.get("message") ?? ""),
    trap: String(form.get("_gotcha") ?? ""),
    render,
  });
  await saveMessage(Object.fromEntries(form), verdict.spam ? "spam" : "received", verdict.reasons);
  return redirect("/thanks", 303);
};
```

Le handler stocke tout avec un statut au lieu de jeter le spam, et répond aux deux avec la même redirection. Vous relisez le spam plus tard, et un bot n'apprend jamais quel signal l'a attrapé. Ajoutez une [limite de débit par visiteur](https://ikiform.com/fr/guides/spam-protection/rate-limiting.md) devant cette route pour arrêter les flots avant que tout cela ne s'exécute.

## Ikiform les exécute sans captcha

Chaque formulaire Ikiform exécute le honeypot, le délai, le score du contenu et les limites de débit sans configuration, et son **Spam check** démarre sur **Off**. Le honeypot est `_gotcha`. Le score du contenu vérifie les liens, 22 formules de spam connues, les domaines d'e-mail jetables et les répétitions identiques sur 24 heures. La limite de débit démarre à 5 par minute par visiteur. À 5 ou plus, un envoi est du spam.

| Signal | Poids |
| --- | --- |
| Honeypot rempli | +5 |
| Envoyé moins de 2 secondes après le rendu signé | +3 |
| Heure de rendu falsifiée ou de plus de 24 heures | +3 |
| Un lien dans un champ de nom ou d'objet | +3 |
| Champs identiques sur 24 heures | +3 |
| Trois liens ou plus | +2 |
| Une formule de spam connue | +2 |
| Un domaine d'e-mail jetable | +2 |

L'heure de rendu signée d'Ikiform vient du schéma public du formulaire. Le [composant shadcn/ui](https://ikiform.com/fr/docs/shadcn.md) l'envoie pour vous. Un formulaire que vous rendez vous-même peut la récupérer avec `getSchema()` de `@ikiform/sdk` et la renvoyer sous la forme `_ikiform_ts` :

`contact.js`:

```js
import { getSchema, submit } from "@ikiform/sdk";

const FORM_ID = "frm_xxxxxxxxxxxxxxxx";
const form = document.querySelector("form");
const schema = await getSchema(FORM_ID);
const rendered = schema.ok ? schema.schema.ts : null;

form?.addEventListener("submit", async (event) => {
  event.preventDefault();
  const data = new FormData(form);
  if (rendered) data.set("_ikiform_ts", rendered);
  const result = await submit(FORM_ID, data);
  form.replaceWith(result.ok ? "Thanks, we'll be in touch." : result.error.message);
});
```

Les formulaires HTML simples n'envoient pas d'heure de rendu et ne sont pas pénalisés pour cela : son absence ne coûte donc rien. Un envoi jugé comme spam reçoit la même réponse de succès qu'un vrai : un bot n'apprend donc rien. Il est stocké avec le statut `spam`, n'envoie ni e-mail de notification ni webhook, ne compte pas dans votre consommation, et est supprimé au bout de 30 jours. Chaque lundi, un espace de travail qui a bloqué du spam reçoit un e-mail récapitulatif avec le total, les formulaires les plus sollicités et les principales raisons. L'[endpoint du schéma](https://ikiform.com/fr/docs/shadcn.md#schema-endpoint) et la [documentation sur le spam](https://ikiform.com/fr/docs/spam.md#scoring) donnent les détails.

> Si quelques bots passent encore, réglez **Spam check** sur **Ikiform**. Cela ajoute Cloudflare Turnstile sans clés, et la plupart des visiteurs ne voient rien. C'est le moment d'ajouter un captcha : quand le dossier de spam le dit, pas avant.

## Vérifiez que tout fonctionne

1. Chargez le formulaire et envoyez-le en moins d'une seconde. Avec votre propre handler, l'envoi est stocké comme spam avec `too_fast`.
2. Modifiez la valeur masquée `rendered` dans les outils de développement du navigateur et envoyez. Le score indique `invalid_render`.
3. Envoyez avec le honeypot rempli, `_gotcha=bot`. L'envoi reçoit quand même une réponse de succès, mais la soumission est stockée comme spam et n'envoie ni e-mail ni webhook.
4. Envoyez un message avec trois liens et rien d'autre de suspect. Il est reçu.
5. Rien n'est arrivé ? Passez le filtre de statut du tableau de bord sur **Spam**. Le spam reçoit la même réponse de succès qu'une vraie soumission.

## Pièges

- **Pages statiques.** Une page construite une fois au déploiement a la même heure de rendu pour chaque visiteur. Signez-la à chaque requête, ou récupérez un nouveau token avec JavaScript quand le formulaire apparaît.
- **Gestionnaires de mots de passe et copier-coller.** Certaines personnes remplissent un formulaire en une seconde grâce au remplissage automatique. Gardez le délai comme signal faible pour qu'il ne bloque jamais seul.
- **Les listes de formules vieillissent.** Les formules de spam changent. Relisez ce qui passe toutes les quelques semaines et ajoutez la formule précise, jamais un mot courant.
- **Les répétitions sont normales.** Les gens renvoient quand ils n'ont pas de réponse. Donnez un poids à une répétition identique, mais ne la bloquez pas d'office.

## Questions

### Puis-je arrêter le spam des formulaires sans captcha ?

En grande partie. Un honeypot, une heure de rendu signée, un score du contenu et une limite de débit arrêtent l'essentiel de ce que reçoit un petit site, sans rien coûter aux vrais visiteurs. Les bots qui pilotent un navigateur complet et attendent peuvent encore passer : c'est là qu'une vérification invisible comme Turnstile aide.

### Combien de temps un piège temporel doit-il attendre ?

Environ trois secondes pour un formulaire de contact. Plus court, il rate les bots rapides ; plus long, il commence à attraper les personnes qui collent un message préparé ou utilisent le remplissage automatique. Utilisez-le comme un signal parmi d'autres, pas comme un blocage strict.

### Pourquoi signer l'horodatage ?

Un horodatage simple dans un champ masqué peut être réécrit par n'importe quel bot pour paraître plus ancien. Le signer sur le serveur avec un secret garantit que seul votre serveur peut produire une heure de rendu valide.

### Ikiform a-t-il besoin d'un captcha pour filtrer le spam ?

Non. Le honeypot, le délai signé, le score du contenu et les limites de débit s'exécutent sur chaque formulaire avec le spam check désactivé. La vérification Turnstile gérée est un supplément que vous activez par formulaire si du spam passe encore.

## Autres guides

- [Arrêter le spam des formulaires de contact](https://ikiform.com/fr/guides/spam-protection.md): Arrêtez le spam des formulaires de contact avec des couches invisibles pour les vrais visiteurs : honeypot, délai, score du contenu, limites de débit, vérification humaine si besoin.
- [Champ honeypot](https://ikiform.com/fr/guides/spam-protection/honeypot.md): Ajoutez à n'importe quel formulaire un honeypot qui attrape les bots sans gêner lecteurs d'écran, clavier ou remplissage automatique, avec la vérification serveur pour chaque stack.
- [Limitation de débit](https://ikiform.com/fr/guides/spam-protection/rate-limiting.md): 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.
- [Alternatives à reCAPTCHA](https://ikiform.com/fr/guides/spam-protection/recaptcha-alternatives.md): Comparez les alternatives à reCAPTCHA pour formulaires, de Turnstile et hCaptcha à la preuve de travail ou à l'absence de captcha, puis migrez avec un petit changement sûr.
- [Cloudflare Turnstile](https://ikiform.com/fr/guides/spam-protection/cloudflare-turnstile.md): Ajoutez Cloudflare Turnstile à un formulaire HTML simple : le widget, siteverify côté serveur, les clés de test, et une façon de l'utiliser sans clés ni serveur à vous.
- [Sites statiques](https://ikiform.com/fr/guides/spam-protection/static-site.md): Arrêtez le spam d'un formulaire de site statique, même sans secret ni compteur, avec un partial honeypot, un script sûr et des vérifications côté serveur.
