Des formulaires sont spammés. Quelqu’un pose alors un captcha que l’extension de formulaires injecte sur toutes les pages du site. Le formulaire de contact est protégé ; la page d’accueil, les articles et les fiches produit chargent désormais un script tiers, une iframe et leurs connexions vers un autre domaine pour un formulaire qu’ils ne contiennent pas.
Ce coût se lit à plusieurs endroits : dans le temps de chargement et l’interactivité, dans le score « Bonnes pratiques » de Lighthouse, dans l’accessibilité du formulaire et, depuis 2025, dans la facture de reCAPTCHA, passé sur Google Cloud avec un quota gratuit plafonné. Ce guide compare reCAPTCHA, Cloudflare Turnstile, hCaptcha et les solutions auto-hébergées sous l’angle de la performance, puis montre comment charger un captcha seulement là où il sert, et seulement quand il sert.
Il pose aussi une question que l’on oublie souvent de poser : faut-il un captcha dans l’absolu ? Avant de lire la suite, faites le compte : sur combien de vos pages le script du captcha se charge-t-il, et pour combien de formulaires ?
Pourquoi un captcha ralentit-il un site ?
Parce qu’un captcha moderne est un script tiers complet : bibliothèque JavaScript, iframe servie par un autre domaine, connexions supplémentaires, parfois cookies tiers. Chargé sur toutes les pages, il ajoute ce travail là où aucun formulaire ne le justifie, et pèse sur le chargement comme sur l’INP, la métrique qui mesure la réactivité aux interactions.
Ce que charge un captcha
Un widget de captcha se compose d’une bibliothèque, chargée depuis le domaine de l’éditeur (www.google.com ou www.gstatic.com pour reCAPTCHA, challenges.cloudflare.com pour Turnstile, js.hcaptcha.com pour hCaptcha), puis d’une iframe qui porte la case à cocher ou le défi invisible. Chaque domaine coûte une résolution DNS, une connexion et une négociation TLS ; l’iframe charge ses propres scripts et exécute ses contrôles sur le processeur du visiteur, en concurrence avec votre page.
reCAPTCHA v3 ajoute un élément visible : un badge fixé en bas de l’écran, posé sur toutes les pages qui chargent le script. Sur mobile, ce badge recouvre parfois un bouton d’appel ou un lien de bas de page. Notre article sur l’impact des JavaScript tiers détaille comment se mesure le coût d’un script tiers ; le captcha en suit la logique, avec une particularité : il ne sert qu’à une action précise, l’envoi d’un formulaire.
Le cas reCAPTCHA v3
reCAPTCHA v3 ne pose aucune question : il calcule un score de 0,0 à 1,0 à partir du comportement observé, et le site décide ensuite. Sa documentation recommande d’appeler la vérification sur les actions à protéger, mais aussi « en arrière-plan des pages » à des fins d’analyse, en expliquant que l’outil fonctionne mieux quand il dispose du maximum de contexte sur les interactions avec le site. Google invite donc lui-même à charger le script sur un maximum de pages.
L’arbitrage est réel. Un score calculé sur un parcours complet est plus fiable qu’un score calculé sur la seule page de contact. Mais ce gain de précision se paie sur chaque page vue, pour tous les visiteurs, alors que seule une petite fraction d’entre eux enverra un formulaire. Sur un site vitrine ou un média, l’arbitrage penche presque toujours vers le chargement limité aux pages à formulaire ; sur un tunnel de paiement attaqué par des robots, le contexte supplémentaire peut se justifier, à condition de l’avoir mesuré.
Les effets de bord mesurables
Le score « Bonnes pratiques » de Lighthouse relève les cookies tiers et l’usage d’API obsolètes. Un captcha en pose souvent ; un script de détection de robots de Cloudflare a d’ailleurs le même effet sur ce score, indépendamment du code du site. Mesure de l’agence du 2 octobre 2026 sur la page de contact d’un site client : hCaptcha reporté à la première interaction, score Bonnes pratiques de 58 à 100, cookies tiers et avertissements d’obsolescence sortis du chargement initial.
Le second effet de bord est visuel. L’iframe du captcha apparaît après le reste du formulaire, et si son conteneur ne lui a pas réservé exactement sa place, tout ce qui suit descend : c’est un décalage de mise en page, compté dans le CLS. Sur le même site, l’iframe était affichée en inline et sa ligne mesurait 85 px, dans un conteneur réservé à 78 px ; passée en display: block, elle tient dans la réserve et rien ne bouge à son apparition. Le CLS et ses causes sont détaillés dans notre guide du CLS.
Qu’est-ce qui a changé pour reCAPTCHA en 2025 ?
reCAPTCHA Classic est passé sur Google Cloud : depuis le quatrième trimestre 2025, Google migre automatiquement les anciennes clés vers des projets Google Cloud, avec un palier gratuit de 10 000 évaluations par mois. Au-delà, la facturation doit être activée, et le quota gratuit s’entend par organisation, ce qui change tout pour une agence.
Le calendrier officiel
Selon la documentation de migration de Google, la création de nouvelles clés Classic a cessé au troisième trimestre 2024. Une première notification invitant à migrer est partie au premier trimestre 2025, une relance au troisième trimestre 2025, et la migration automatique des clés a commencé au quatrième trimestre 2025. Elle s’achève au premier trimestre 2026 avec le blocage de l’accès à l’API pour les clés sans projet Google Cloud.
La migration automatique crée un projet Google Cloud pour les clés et envoie une invitation à en accepter la propriété. Les clés existantes, v2 comme v3, continuent de fonctionner sans changement de code, mais elles ne se gèrent plus qu’une fois le projet accepté. Au moment où nous écrivons, la documentation de Google présente d’ailleurs reCAPTCHA sous un nouveau nom de gamme, Google Cloud Fraud Defense, ce qui ne simplifie pas les recherches.
Ce qui se passe au-delà du quota
La même documentation décrit deux comportements selon l’API utilisée, faute de facturation activée. Avec l’ancienne API siteverify, les requêtes au-delà du quota « échouent ouvertes » : elles renvoient un succès avec un score de 0,9 et un message de dépassement, autrement dit la protection laisse passer tout le monde sans le dire au formulaire. Avec l’API CreateAssessment, elles échouent avec une erreur 429 et aucune évaluation n’est créée.
Le tarif est publié sur la page de comparaison des offres, consultée le 15 octobre 2026 : offre Essentials gratuite jusqu’à 10 000 évaluations par mois calendaire ; offre Premium gratuite jusqu’à 10 000, puis 8 dollars forfaitaires de 10 001 à 100 000 évaluations, puis 1 dollar les 1 000 au-delà. Un site qui reçoit 30 000 envois de formulaire par mois, ou qui suit la recommandation d’appeler reCAPTCHA en arrière-plan sur ses pages, franchit le palier gratuit sans s’en apercevoir.
Un quota par organisation : le piège des agences
La même page tranche la question que se posent les agences : les 10 000 évaluations gratuites s’entendent par organisation Google Cloud, et la limite cumule l’usage de tous les comptes et de tous les sites. Une agence qui a créé les clés reCAPTCHA de vingt clients sous son propre compte se retrouve avec un seul quota gratuit pour vingt sites, et la facture, ou l’échec silencieux décrit plus haut, qui va avec.
La conséquence pratique tient en deux gestes. Inventorier les clés reCAPTCHA de chaque site et l’organisation Google Cloud qui les porte, puis rattacher chaque projet au compte Google du client, propriétaire de son site et de sa facture. C’est aussi l’occasion de se demander si le site a besoin de reCAPTCHA, ou d’une protection qui ne dépend d’aucun quota.
Quelles alternatives à reCAPTCHA ?
Trois familles coexistent : les défis invisibles d’autres éditeurs (Cloudflare Turnstile, hCaptcha), les preuves de travail auto-hébergées (ALTCHA, Cap) ou européennes (Friendly Captcha), et les défenses sans interface, honeypot (« pot de miel » en français) et délai minimal. Leur coût de performance dépend surtout du domaine qui sert le script et du moment où il se charge.
Les solutions comparées
Le tableau ci-dessous reprend les informations publiées par chaque éditeur, consultées le 15 octobre 2026. Une preuve de travail fait résoudre au navigateur un petit calcul cryptographique, invisible pour le visiteur, coûteux pour un robot qui envoie des milliers de formulaires. La dernière colonne vient d’une mesure de l’agence du 15 octobre 2026 : octets reçus d’un autre domaine sur la page du formulaire, iframe comprise, et temps d’exécution relevé par Lighthouse 13.5.0 en émulation mobile.
| Solution | Détection | Interaction visible | Origine des scripts | Tarif et quota publiés | Rendu explicite | Coût mesuré (page du formulaire) |
|---|---|---|---|---|---|---|
| reCAPTCHA v3 (Google Cloud) | Score comportemental | Aucune, badge fixe | Domaines Google | 10 000 évaluations gratuites par mois et par organisation, puis payant | Oui | 449 Ko, 262 ms d’exécution |
| Cloudflare Turnstile | Contrôles JavaScript, dont preuve de travail | Case à cocher seulement si le risque l’exige | challenges.cloudflare.com | Gratuit, défis illimités, 20 widgets et 10 noms d’hôte par widget | Oui, render=explicit | 135 Ko, moins de 5 ms hors iframe |
| hCaptcha | Défi et signaux de risque | Case à cocher, défi visuel possible | js.hcaptcha.com | Offre gratuite, offres payantes | Oui, render=explicit | 303 Ko, 75 à 109 ms, 2 cookies tiers |
| Friendly Captcha | Preuve de travail | Aucune | Domaine de l’éditeur, européen | Gratuit pour un projet non commercial (1 000 requêtes par mois), sinon payant | Oui | Non mesuré |
| ALTCHA | Preuve de travail | Case à cocher | Votre domaine (auto-hébergé) | Libre (MIT), offres hébergées payantes | Composant web | 0 Ko tiers (36 Ko servis par le site), 6 à 7 ms |
| Cap | Preuve de travail | Case à cocher | Votre domaine (auto-hébergé) | Libre (Apache 2.0) | Composant web | Non mesuré |
| Pot de miel et délai minimal | Champ piège, temps de remplissage | Aucune | Aucun script tiers | Gratuit | Sans objet | 0 Ko, aucun script |
Deux lignes du tableau se lisent de près. Turnstile est gratuit et sans plafond de défis, avec 20 widgets par compte sur l’offre gratuite selon la page mise à jour le 14 août 2026 : une agence qui gère un parc de sites sous un seul compte atteint vite cette limite. Les solutions auto-hébergées, elles, suppriment les domaines tiers : le script vient de votre domaine, sur une connexion déjà ouverte, et aucune donnée ne part chez un éditeur.
Ce que montre la mesure de l’agence
Banc du 15 octobre 2026 : WordPress 7.1.3, une même page de contact, cinq solutions, trois modes de chargement, cinq audits Lighthouse par variante, puis un parcours scripté en 4G lente avec envoi vérifié côté serveur, 65 sur 65. La page de contact pèse 27,9 Ko avec un pot de miel, 63,7 Ko avec ALTCHA, 163,2 Ko avec Turnstile, 331,2 Ko avec hCaptcha et 476,8 Ko avec reCAPTCHA v3.
L’iframe compte pour beaucoup : 193 Ko pour hCaptcha, 112 Ko pour Turnstile. Or l’insight tiers de Lighthouse et l’attribution des frames longues ignorent le travail d’un document chargé dans l’iframe d’un autre domaine : le temps d’exécution du tableau est celui de la page, celui du widget s’y ajoute. Turnstile et hCaptcha ont été mesurés avec les clés de test, dont l’iframe peut différer de celle d’une clé réelle.
La note Performance est restée à 100 et le TBT à 0 dans presque tous les cas : sur une page légère, un captcha ne change pas la note. Les écarts se lisent dans les octets, l’exécution et les Bonnes pratiques, 58 pour hCaptcha, avec deux cookies tiers posés depuis son domaine et un avertissement d’obsolescence de l’API Protected Audience. Les éditeurs annoncent tous une solution « légère » : la mesure départage ces promesses.
Votre site est-il aussi rapide que vos visiteurs l’espèrent ?
Comment charger un captcha sans pénaliser les autres pages ?
Uniquement sur les pages qui portent un formulaire, et seulement quand le visiteur s’en approche. Turnstile et hCaptcha documentent un mode de rendu explicite (render=explicit) qui permet de charger la bibliothèque à la demande, puis de rendre le widget dans un conteneur choisi : la page d’accueil ne charge alors plus rien.
Sur WordPress, un chargement par page
Les extensions de formulaires et d’anti-spam enfilent souvent leur script sur toutes les pages, sans réglage pour le limiter. La méthode générique consiste à retirer le script par défaut, puis à le réenfiler au moment où le formulaire est rendu : le script suit alors le formulaire, y compris quand celui-ci est inséré dans un bloc réutilisable ou un widget. Notre article sur les Core Web Vitals sur WordPress replace cette méthode parmi les autres réglages. Un point de vigilance : une page de connexion peut dépendre du script d’un anti-spam, et refuser toute connexion sans lui.
Un chargement à l’approche du formulaire
Sur une page qui porte un formulaire en bas, le chargement peut encore attendre que le visiteur s’en approche. Le chargeur ci-dessous combine deux déclencheurs : un IntersectionObserver qui charge la bibliothèque quand le formulaire arrive à 300 px de l’écran, et un écouteur focusin qui la charge dès que le visiteur entre dans un champ, au clavier ou par un lien d’ancre. La bibliothèque est chargée en mode explicite, avec un rappel qui rend le widget dans chaque conteneur.
<form id="contact" method="post" action="/envoi">
<label for="email">Courriel</label>
<input id="email" name="email" type="email" autocomplete="email" required>
<!-- Conteneur du widget : fournisseur et clé de site en attributs, place réservée -->
<div class="captcha" data-captcha="turnstile" data-sitekey="VOTRE-CLE-DE-SITE"></div>
<button>Envoyer</button>
</form>
<script src="/js/captcha-a-la-demande.js" defer></script>
// /js/captcha-a-la-demande.js
// Charge le captcha quand le visiteur s'approche du formulaire ou y entre, puis rend le widget.
(function () {
const FOURNISSEURS = {
turnstile: {
script: 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit&onload=awpCaptchaPret',
rendre: (el) => window.turnstile.render(el, { sitekey: el.dataset.sitekey }),
},
hcaptcha: {
script: 'https://js.hcaptcha.com/1/api.js?render=explicit&onload=awpCaptchaPret&recaptchacompat=off',
rendre: (el) => window.hcaptcha.render(el, { sitekey: el.dataset.sitekey }),
},
};
const conteneurs = [...document.querySelectorAll('[data-captcha]')];
if (!conteneurs.length) return; // page sans formulaire protégé : rien à charger
const fournisseur = FOURNISSEURS[conteneurs[0].dataset.captcha];
let demande = false;
// Rappel appelé par la bibliothèque une fois prête (paramètre onload=) : rendu de chaque conteneur.
window.awpCaptchaPret = () => conteneurs.forEach((el) => fournisseur.rendre(el));
function charger() {
if (demande) return; // un seul chargement, quel que soit le déclencheur
demande = true;
observateur.disconnect();
const s = document.createElement('script');
s.src = fournisseur.script;
s.async = true;
document.head.append(s);
}
// Déclencheur 1 : le formulaire arrive à 300 px de l'écran.
const observateur = new IntersectionObserver((entrees) => {
if (entrees.some((e) => e.isIntersecting)) charger();
}, { rootMargin: '300px 0px' });
conteneurs.forEach((el) => observateur.observe(el.closest('form') || el));
// Déclencheur 2 : le visiteur entre dans un champ (clavier, lien d'ancre, remplissage automatique).
conteneurs.forEach((el) => (el.closest('form') || el).addEventListener('focusin', charger, { once: true }));
})();
Testé le 15 octobre 2026 dans Chromium 141 avec les clés de test publiées par Cloudflare et hCaptcha, sur une page dont le formulaire se trouve à 2 500 px du haut : aucune requête vers l’éditeur au chargement, puis bibliothèque chargée et widget rendu au défilement comme à l’entrée dans le champ. La documentation de Turnstile conseille au contraire d’exécuter son script le plus tôt possible : le chargement à l’approche est un compromis assumé, et le temps de résolution du défi doit rester inférieur au temps de remplissage du formulaire, ce qui se vérifie sur le site.
Mesuré le 15 octobre 2026 avec des clés réelles et ce chargeur, sur 4 à 5 essais par éditeur : Turnstile rend son jeton 1 à 2,3 secondes après l’entrée dans le formulaire en 4G, sans interaction, et parfois avant. hCaptcha, en offre gratuite, a imposé des défis visuels qui ont porté ce délai à 18 et 42 secondes, au-delà du temps de remplissage d’un formulaire de contact : ce délai tient au défi lui-même, quel que soit le moment du chargement.
Pour la recette, une précaution : Turnstile refuse un navigateur qui se fait passer pour un autre appareil (émulation des outils de développement, erreur 600010) et affiche sa case à un navigateur automatisé. Un test fait avec ces outils ne reflète pas l’expérience d’un visiteur ; la vérification se fait aussi sur un vrai téléphone.
Sur le banc, limiter le script aux pages à formulaire supprime toute requête vers l’éditeur sur la page d’accueil, pour les cinq solutions, et remonte la note Bonnes pratiques de cette page à 100 avec hCaptcha. Le chargement à l’approche enlève ensuite le captcha du chargement de la page de contact. Lighthouse ne fait pas défiler la page : dans ce mode, l’audit ne voit plus le captcha, et seul un parcours mesure son coût.
Ce mode a une limite : un script lourd chargé au dernier moment pèse sur la saisie. Avec reCAPTCHA v3, l’INP de la saisie est passé de 56 à 144 ms en 4G lente, parce que 350 Ko arrivent et s’exécutent pendant que le visiteur tape ; 72 ms avec hCaptcha, 56 ms inchangés avec Turnstile et ALTCHA. Avec un script lourd, déclencher le chargement plus tôt en élargissant la marge de l’IntersectionObserver, ou choisir une solution légère.
Le piège de l’appel en ligne différé séparément
Les extensions de performance proposent de reporter les scripts tiers à la première interaction. Appliqué à un captcha, le réglage peut faire disparaître le composant sans la moindre erreur visible. Le code d’intégration d’un captcha tient souvent en deux parties : la bibliothèque, et un appel en ligne qui l’utilise (grecaptcha.render(…), grecaptcha.execute(…)). Si seule la bibliothèque est reportée, l’appel s’exécute au chargement sur une bibliothèque absente, échoue, et la bibliothèque arrivée plus tard ne le rejoue jamais.
Le risque dépend du mode de report de l’extension. Cas réel, en septembre 2026, sur un site dont toutes les demandes passaient par des formulaires HubSpot : la bibliothèque des formulaires reportée par une liste de motifs, l’appel de création resté au chargement, et des formulaires absents pendant six jours avant que le client ne s’en aperçoive. Les contrôles faits après le réglage portaient sur les métriques, qui s’étaient améliorées, précisément parce que le composant ne se chargeait plus.
| Mode de report | Appels en ligne | Risque pour le captcha |
|---|---|---|
| Report de tout le JavaScript (WP Rocket, « Delay JavaScript execution ») | Reportés aussi, rejoués dans l’ordre du document | Faible : bibliothèque puis appel |
| Report d’une liste de motifs (FlyingPress, Perfmatters) | Reportés seulement s’ils contiennent un motif de la liste | Élevé : la bibliothèque part, l’appel reste |
| Chargeur maison | Selon leur marquage | Élevé si l’appel n’attend pas la bibliothèque |
Bibliothèque qui se rend seule (render=onload) | Aucun appel en ligne | Faible : le widget apparaît à l’interaction |
La règle qui en découle : un réglage qui change le moment d’exécution des scripts se valide par la fonction, jamais par la mesure. Après chaque réglage, vérifier que le widget existe quelques secondes après le chargement et après une interaction, sur ordinateur et sur mobile.
Le contrôle ci-dessous, à coller dans la console de la page du formulaire, signale un conteneur resté vide. Chercher seulement une iframe donne un faux négatif avec reCAPTCHA v3, qui n’a pas de widget, et avec ALTCHA, qui rend un composant web ; ce contrôle couvre les deux, vérifiée le 15 octobre 2026 dans Chromium 141 avec Turnstile, ALTCHA 3.4.0 et reCAPTCHA.
// Contrôle fonctionnel après report : chaque captcha est-il prêt ?
// À lancer quelques secondes après le chargement, puis après un clic ou un défilement.
// Turnstile rend son iframe dans une racine fantôme fermée : le champ du jeton fait foi.
// ALTCHA est un composant web : sa définition fait foi. reCAPTCHA v3 n'a pas de widget.
const pret = (el) =>
el.localName === 'altcha-widget'
? !!customElements.get('altcha-widget')
: !!el.querySelector('iframe, input[name="cf-turnstile-response"], textarea[name="h-captcha-response"], textarea[name="g-recaptcha-response"]');
console.table([...document.querySelectorAll('[data-captcha], .cf-turnstile, .h-captcha, .g-recaptcha, altcha-widget')].map((el) => ({
conteneur: el.dataset.captcha || el.className || el.localName,
widget: pret(el),
})));
console.log('reCAPTCHA v3 prêt :', typeof window.grecaptcha?.execute === 'function');
Réserver la place du widget
Le widget apparaît après le reste du formulaire : sa place se réserve dans le CSS, par une hauteur minimale sur le conteneur, égale à celle du widget rendu. Mesuré le 15 octobre 2026 dans Chrome 155, à 375 et 1 280 px : 68,5 px pour Turnstile, 76 px pour hCaptcha, 78 px pour reCAPTCHA v2 en taille normale, 138 à 144 px en taille compacte.
Le conteneur et l’iframe passent en display: block : affichée en ligne, l’iframe hérite de la hauteur de ligne du texte et déborde de la réserve de quelques pixels, assez pour produire un décalage. Avec hCaptcha, le conteneur passe ainsi de 79,5 à 76 px. Turnstile place son iframe dans une racine fantôme fermée, hors d’atteinte du CSS de la page : sa réserve suit la mesure, 68,5 px, plus haute que les 65 px annoncés.
Un captcha peut-il être accessible ?
Un défi visuel seul exclut une partie des visiteurs : personnes aveugles ou malvoyantes, personnes ayant des troubles cognitifs. Le W3C le documente depuis des années, et les solutions invisibles réduisent le problème sans le supprimer : que se passe-t-il pour le visiteur quand la détection échoue ?
La note du W3C sur l’inaccessibilité des CAPTCHA, dans sa version du 16 décembre 2021, conclut que la nature même d’un défi interactif exclut de nombreuses personnes handicapées. Elle recommande de limiter les captchas au strict nécessaire, de proposer plusieurs modalités quand un défi est indispensable, et préfère un pot de miel tant qu’aucune attaque ne l’impose. Le critère 1.1.1 des WCAG 2.2 exige, pour un captcha, une description de son objet et des formes alternatives dans d’autres modalités sensorielles.
Turnstile se déclare conforme aux WCAG 2.2 AA, ALTCHA aussi ; ces déclarations restent celles des éditeurs. Le point faible des solutions invisibles est leur cas d’échec : un visiteur classé comme suspect par un score ambigu se voit présenter un défi, ou se voit refuser l’envoi, sans recours. La note du W3C demande justement de documenter ce cas et d’offrir une voie secondaire, comme une adresse de contact.
Depuis le 28 juin 2025, l’European Accessibility Act étend les obligations d’accessibilité à une partie du secteur privé ; notre article sur le RGAA et l’accessibilité numérique précise qui est concerné et ce qui est exigé.
Captcha et RGPD : quelles précautions ?
Un captcha tiers transmet des données de navigation à son éditeur. La CNIL l’exempte de consentement quand sa seule finalité est la sécurité du site, mais cite explicitement reCAPTCHA parmi les solutions dont l’éditeur réutilise les données pour son propre compte. Ce qui suit est un repère de lecture : seul un juriste tranche un cas précis.
Dans sa foire aux questions sur les traceurs (question 17), la CNIL indique qu’un dispositif anti-robot dont la seule finalité est la sécurisation du site ou d’un mécanisme d’authentification ne requiert pas de consentement. Lorsque le fournisseur poursuit d’autres finalités, le consentement redevient nécessaire, et la CNIL relève elle-même la contradiction : un simple refus suffirait alors à contourner la protection. Elle recommande dans ce cas des solutions alternatives ou complémentaires. Une solution auto-hébergée ou une défense sans service tiers évite la question ; notre guide des CMP traite du consentement côté performance.
Faut-il vraiment un captcha sur votre formulaire ?
Souvent, non. Une défense en couches invisible, pot de miel, délai minimal, limitation de débit et pare-feu, arrête la plupart des robots qui ciblent un formulaire de contact ; le captcha devient alors le dernier recours, réservé aux formulaires réellement attaqués, et chargé à la demande.
Pot de miel et délai minimal
Un pot de miel est un champ invisible pour les humains, que les robots remplissent parce qu’ils remplissent tout. Un délai minimal complète le dispositif : un formulaire envoyé moins de trois secondes après son affichage n’a pas été rempli par une personne. Le code ci-dessous signe l’heure d’affichage pour qu’un robot ne puisse pas la falsifier, et sort le champ piège de la tabulation et des lecteurs d’écran. Testé le 15 octobre 2026 sous PHP 8.3 : envoi immédiat refusé, champ piège rempli refusé, horodatage falsifié refusé, envoi après trois secondes accepté.
<?php
// Pot de miel + délai minimal de soumission, sans service tiers.
const SECRET_FORMULAIRE = 'remplacer-par-une-valeur-aleatoire-longue';
const DELAI_MINIMAL = 3; // secondes
const DELAI_MAXIMAL = 7200; // un formulaire ouvert depuis plus de 2 h se recharge
function jeton_affichage(): string {
$t = (string) time();
return $t . '.' . hash_hmac( 'sha256', $t, SECRET_FORMULAIRE ); // heure d'affichage signée
}
function envoi_humain( array $post ): bool {
if ( '' !== trim( $post['site_web'] ?? '' ) ) {
return false; // pot de miel rempli
}
[ $t, $signature ] = array_pad( explode( '.', $post['affiche_le'] ?? '', 2 ), 2, '' );
if ( ! hash_equals( hash_hmac( 'sha256', $t, SECRET_FORMULAIRE ), $signature ) ) {
return false; // horodatage absent ou falsifié
}
$age = time() - (int) $t;
return $age >= DELAI_MINIMAL && $age <= DELAI_MAXIMAL;
}
if ( 'POST' === $_SERVER['REQUEST_METHOD'] && ! envoi_humain( $_POST ) ) {
http_response_code( 400 );
exit( 'Envoi refusé. Patientez quelques secondes avant de valider le formulaire.' );
}
?>
<form method="post">
<label for="email">Courriel</label>
<input id="email" name="email" type="email" autocomplete="email" required>
<!-- Pot de miel : hors écran, hors tabulation, ignoré des lecteurs d'écran -->
<div aria-hidden="true" style="position:absolute;left:-10000px">
<label for="site_web">Ne pas remplir</label>
<input id="site_web" name="site_web" type="text" tabindex="-1" autocomplete="off">
</div>
<input type="hidden" name="affiche_le" value="<?= htmlspecialchars( jeton_affichage() ) ?>">
<button>Envoyer</button>
</form>
Limitation de débit dans l’application
Un formulaire qui envoie un courriel ou un SMS est un service d’envoi ouvert à tous. La limitation de débit au serveur web compte des requêtes par adresse IP ; un robot qui change d’adresse à chaque envoi passe dessous.
La limitation dans l’application compte aussi par cible, l’adresse e-mail ou le numéro saisis, sur une heure et sur 24 heures, et répond par un code 429 accompagné d’un en-tête Retry-After et d’un message lisible. Convention de l’agence pour un formulaire de contact : 5 envois par heure et 20 par jour par adresse IP, 3 et 10 par cible.
Pare-feu applicatif : bloquer plutôt que défier
Un pare-feu applicatif, celui de Cloudflare par exemple, arrête en amont les robots connus et les volumes anormaux. Un point de vigilance, constaté en septembre 2026 sur un site de contenus : un jeu de règles réglé en défi JavaScript sur admin-ajax.php présentait environ 438 000 défis par jour à de vrais visiteurs, pour 14 résolus. Un défi ne se résout pas dans un appel AJAX, qui attend du JSON et reçoit une page HTML : sur une requête qui n’est pas une page, un défi équivaut à un blocage des visiteurs légitimes.
Si le captcha reste nécessaire
Quand un formulaire reste attaqué malgré ces couches, le captcha revient, chargé à la demande comme plus haut, et vérifié côté serveur. La vérification du jeton est obligatoire : un widget affiché ne protège rien si le serveur ne contrôle pas le jeton auprès de l’éditeur. Turnstile et hCaptcha attendent un POST encodé en formulaire, et un jeton Turnstile expire après 300 secondes et ne se valide qu’une fois. La fonction ci-dessous a été testée le 15 octobre 2026 avec les clés de test des deux éditeurs : jeton valide accepté, clé de refus et jeton vide refusés.
<?php
// Vérification côté serveur du jeton (Turnstile ou hCaptcha), avant tout traitement du formulaire.
function verifier_captcha( string $fournisseur, string $secret, string $jeton, string $ip = '' ): bool {
$points = array(
'turnstile' => 'https://challenges.cloudflare.com/turnstile/v0/siteverify',
'hcaptcha' => 'https://api.hcaptcha.com/siteverify',
);
if ( '' === $jeton || ! isset( $points[ $fournisseur ] ) ) {
return false; // aucun jeton : widget absent ou non résolu
}
$champs = array( 'secret' => $secret, 'response' => $jeton );
if ( '' !== $ip ) {
$champs['remoteip'] = $ip; // facultatif, améliore la détection
}
$ch = curl_init( $points[ $fournisseur ] );
curl_setopt_array( $ch, array(
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query( $champs ), // application/x-www-form-urlencoded
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
) );
$corps = curl_exec( $ch );
curl_close( $ch );
if ( false === $corps ) {
return false; // service injoignable : refuser, ou basculer sur une autre défense
}
$reponse = json_decode( $corps, true );
return ! empty( $reponse['success'] );
}
// Champ du jeton : cf-turnstile-response (Turnstile) ou h-captcha-response (hCaptcha).
$jeton = $_POST['cf-turnstile-response'] ?? $_POST['h-captcha-response'] ?? '';
if ( ! verifier_captcha( 'turnstile', getenv( 'TURNSTILE_SECRET' ), $jeton, $_SERVER['REMOTE_ADDR'] ?? '' ) ) {
http_response_code( 400 );
exit( 'Vérification anti-robot échouée. Rechargez la page et réessayez.' );
}
Inventorier les scripts tiers d’un site, captcha compris, et décider lesquels méritent leur place, c’est l’objet d’un audit de performance. Les widgets du même ordre, chat, avis clients, prise de rendez-vous, obéissent aux mêmes règles, comme le montre notre article sur les widgets de relation client ; leur mise en place sans régression se travaille en accompagnement technique. Reste la question du début : votre captcha se charge-t-il pour vos formulaires, ou pour tout le site ?