Nous developpons votre site vitrine SPA À PARTIR DE 300€. Oui, c'est dingue. Web SPA À PARTIR DE 300€ — Oui, c'est dingue. Parlons-en →

Comment se protéger des bots avec Turnstile ou reCAPTCHA et pouvoir changer de fournisseur

Comment se protéger des bots avec Turnstile ou reCAPTCHA et pouvoir changer de fournisseur

Personne ne choisit un fournisseur de captcha en pensant au jour où il voudra en changer, et pourtant ce jour arrive : le prix monte, le taux de faux positifs se dégrade, ou le client exige que ses données ne transitent pas par tel serveur. Voici comment l'intégrer dès le départ pour que ce changement tienne en une ligne de configuration et non en une chasse à travers tout le projet, et où placer la vérification pour qu'elle serve à quelque chose. Les exemples sont en Laravel, l'une de nos spécialités chez AndorraDev, et nous comparons avec ce qui existe déjà dans son écosystème, mais l'abstraction proposée vaut pour n'importe quel framework.

Poser un captcha est facile, c'est le changer qui fait mal

Intégrer Turnstile prend quinze minutes. Vous copiez le div, vous chargez le script, vous validez le jeton contre l'endpoint du fournisseur et c'est réglé.

Le problème apparaît un an plus tard, quand vous voulez changer. Et ce n'est pas un problème d'effort, c'est un problème de surface de contact : le fournisseur n'apparaît pas à un seul endroit, il apparaît à quatre.

Le nom du champ change. Turnstile envoie cf-turnstile-response, reCAPTCHA envoie g-recaptcha-response. Cette chaîne est écrite dans votre formulaire, dans votre règle de validation et dans votre JavaScript.

L'URL du script change, et elle se trouve dans le layout ou dans la vue.

Le balisage change. Une classe cf-turnstile n'est pas une classe g-recaptcha.

Et le flux change d'une version à l'autre : la version invisible de reCAPTCHA n'affiche aucun encadré, elle s'exécute à l'envoi du formulaire et injecte le jeton, ce qui demande un JavaScript dont la précédente n'avait pas besoin.

Multipliez cela par le nombre de formulaires que vous avez et ce n'est plus un changement de fournisseur, c'est un après-midi.

Cinq méthodes suffisent pour abstraire un captcha

Ce qu'il faut isoler est exactement ce qui varie, et cela se révèle très peu de choses :

interface CaptchaDriver
{
    public function name(): string;
    public function verify(string $token, ?string $ip = null): VerificationResult;
    public function siteKey(): string;
    public function scriptUrl(): string;
    public function responseField(): string;
}

Avec cela, changer de fournisseur revient à changer une variable d'environnement, à condition qu'aucune chaîne littérale ne soit restée dans votre application.

Et c'est là que se joue la clé pratique : l'abstraction ne fonctionne que si vous l'utilisez entièrement. Si vous affichez le widget à la main et écrivez le nom du champ dans le contrôleur, vous avez l'interface en place et le couplage intact. Cela vaut la peine de le vérifier avant de considérer l'intégration comme terminée : il suffit de chercher dans le projet les chaînes du fournisseur et de vérifier qu'il n'en reste aucune.

Le jeton peut être réutilisé si vous ne l'empêchez pas

La documentation des fournisseurs s'arrête à « le jeton est valide », et c'est là que presque tout le monde s'arrête aussi. Le problème est que valide ne veut pas dire neuf, et un captcha qui accepte deux fois le même jeton n'arrête personne qui prenne la peine de regarder.

Un jeton de captcha est valide pendant quelques minutes. Si votre serveur vérifie seulement qu'il est valide, un script peut résoudre le captcha une fois et renvoyer le même jeton de nombreuses fois à l'intérieur de cette fenêtre. Vous avez vérifié qu'il y a eu un humain, pas qu'il y a un humain à chaque envoi.

La protection est simple et il faut la mettre en place volontairement : enregistrer une empreinte du jeton au moment de le valider et le refuser s'il réapparaît.

$cacheKey = 'captcha|' . md5($token);

if (Cache::has($cacheKey)) {
    $fail('captcha.used');
    return;
}

// ... vérification auprès du fournisseur ...

Cache::put($cacheKey, true, now()->addMinutes(5));

Le délai doit ressembler à la validité du jeton lui-même. Cinq minutes correspondent bien à Turnstile, dont les jetons durent environ trois cents secondes.

Andie recommande

La protection anti-rejeu est ce qui empêche qu'un jeton résolu une fois serve à plusieurs envois. Si votre formulaire est renvoyé sans rechargement de page, videz le champ caché du jeton après chaque envoi pour que le JavaScript en génère un nouveau. Deux lignes, et la protection fait son travail sans que l'utilisateur s'en aperçoive.

Vérifiez l'action, pas seulement le score

Ceci est propre à la version qui note le comportement au lieu d'afficher un encadré, et c'est là que le plus de monde s'arrête trop tôt.

Cette version renvoie trois choses : si le jeton est valide, un score de zéro à un, et l'action que vous avez déclarée en le générant. Presque tout le monde vérifie le score et oublie l'action.

Le problème, c'est qu'un jeton généré sur n'importe quelle page de votre site est valide sur n'importe quelle autre. Si vous avez un formulaire d'inscription à la newsletter avec un seuil bas et un formulaire de création de compte avec un seuil élevé, un attaquant peut miner des jetons sur le formulaire bon marché et les dépenser sur le formulaire cher. Idem pour le nom de domaine : si vous ne vérifiez pas que le jeton a été généré sur votre site, un jeton généré ailleurs fera l'affaire.

Le vérifier, c'est un argument de plus à deux endroits. Le widget marque le jeton avec l'action de ce formulaire, et la règle exige cette même action :

<x-laracaptcha::widget action="register" />
'g-recaptcha-response' => ['required', Captcha::make('recaptcha_v3', 'register')],

Un jeton frappé pour autre chose est rejeté avec action-mismatch, et un jeton qui arrive sans action aussi. Les fournisseurs dont les jetons n'en portent pas, Turnstile parmi eux, l'ignorent sans que vous ayez à changer quoi que ce soit en passant de l'un à l'autre.

Un captcha protège l'étape coûteuse, pas le formulaire

On a tendance à voir le captcha comme un videur à l'entrée : on le place à la porte et l'affaire est réglée. Mais tous les formulaires ne coûtent pas la même chose à attaquer ni à défendre, et poser le filtre au mauvais endroit gêne les utilisateurs légitimes sans arrêter ceux qui comptent.

La réaction par défaut consiste à placer le captcha sur le formulaire de connexion. Et bien souvent c'est une erreur, parce que la connexion est déjà protégée par autre chose : pour entrer il faut trouver un mot de passe, et pour cela la limite de tentatives est une meilleure défense et ne gêne personne.

Ce que vous voulez vraiment protéger, c'est l'opération qui vous coûte de l'argent ou de la réputation. Dans un de nos projets, le captcha ne protège pas la connexion : il protège l'étape qui précède la création de compte, juste avant l'envoi du code de vérification par courriel. La raison est écrite dans le contrôleur lui-même et elle est exacte : pour ne pas envoyer de courriels à des bots. Si l'adresse existe déjà, c'est une connexion normale et le captcha n'apparaît même pas.

C'est la bonne question : non pas "quel formulaire est-ce que je protège", mais "quelle action se déclenche à l'envoi". Si l'action consiste à écrire une ligne, la limite de requêtes suffit en général. S'il s'agit d'envoyer un courriel, d'appeler un modèle payant ou de créer une ressource chez un tiers, là oui.

Décidez ce qui se passe quand le fournisseur ne répond pas

Ajouter un captcha revient à placer un tiers sur le chemin critique de vos formulaires. Presque personne ne décide de ce qui doit arriver si ce tiers tombe, et la décision finit par être prise par le code, au hasard, en général de la pire façon.

Vérifier un captcha, c'est un appel HTTP vers un service externe. Si ce service tarde, votre requête attend, et s'il ne répond pas, il faut bien que quelque chose se passe. Les deux options :

Échouer en mode fermé : sans vérification, on ne passe pas. C'est l'option sûre et cela signifie qu'une panne du fournisseur met vos formulaires à terre.

Échouer en mode ouvert : si la vérification est impossible, on laisse passer et on journalise. Vous maintenez le service et vous acceptez une fenêtre pendant laquelle tout entre.

Ce qui n'est pas acceptable, c'est que la décision soit prise par une erreur non maîtrisée et que l'utilisateur reçoive une erreur du serveur. Une vérification impossible à terminer doit arriver dans votre règle de validation comme une vérification échouée, pas comme une exception.

Et dans le même lot se trouve le délai d'attente maximal. Cinq secondes suffisent largement pour un siteverify ; les trente que vous héritez du client HTTP si vous ne le fixez pas retiennent un processus PHP bien trop longtemps, et avec du trafic cela se remarque avant tout le reste.

Il y a en plus un cas intermédiaire presque toujours oublié : une réponse qui arrive mais n'est pas du JSON, la page d'erreur d'un intermédiaire par exemple. Ce n'est pas un échec de connexion, il faut donc la traiter à part, et le bon comportement est qu'elle ne passe pas non plus : elle se décode en vide, ne porte aucune confirmation de succès et échoue donc pareillement.

Ces trois décisions sont prises d'office chez nous : échec en mode fermé, avec cinq secondes configurables, et une réponse qui n'est pas du JSON ne passe pas davantage. Si votre formulaire a besoin du contraire, cela se change ; ce que vous n'aurez jamais, c'est un cinq cents parce que Cloudflare met du temps à répondre.

Ce qui existe sur le marché et ce qui les distingue

L'écosystème est bien fourni et les options se classent par degré de couplage.

Les paquets dédiés à un seul fournisseur sont les plus nombreux et fonctionnent très bien dans leur périmètre. ryangjchandler/laravel-cloudflare-turnstile couvre Turnstile avec une ergonomie très soignée, en particulier pour les tests, où son simulateur permet de forcer un échec ou une expiration. anhskohbo/no-captcha est le vétéran de reCAPTCHA et cumule des millions d'installations. Le compromis des deux est le même et c'est le sujet de cet article : changer de fournisseur, c'est réécrire des formulaires, des règles et des vues, pas changer une variable.

La bibliothèque officielle de Google expose la vérification dans le détail : action, nom de domaine et seuil sont exigés dans l'appel lui-même. Ce qu'elle n'apporte pas, c'est tout ce qui touche à Laravel : ni règle de validation, ni composant, ni facade. C'est vous qui écrivez l'intégration et vous restez lié à reCAPTCHA.

Le champ piège est une autre couche, différente et très sous-estimée : un champ caché qu'un humain ne remplit jamais, plus un horodatage. Zéro JavaScript tiers, zéro problème avec la réglementation sur les données et coût opérationnel nul. Il arrête les bots génériques, il n'arrête pas les bots ciblés. Il ne concurrence pas un captcha, il s'y ajoute.

Et la limite de requêtes, vous l'avez déjà dans le framework. Elle ne distingue pas un humain d'un bot, elle limite seulement la fréquence, et pour beaucoup de formulaires c'était tout ce qu'il fallait.

Et si vous ne travaillez pas avec Laravel

Les critères sont les mêmes dans n'importe quel langage :

  • Isolez ce qui varie, c'est-à-dire le nom du champ, la clé publique, l'URL du script et le balisage. Cela fait quatre choses.
  • Et ensuite servez-vous-en vraiment. Une interface avec des chaînes littérales éparpillées dans les vues ne vous sauvera pas le jour du changement.
  • Empêchez la réutilisation du jeton. Une empreinte en cache et c'est réglé.
  • Vérifiez l'action et le domaine, pas seulement le score.
  • Placez le captcha devant l'opération coûteuse, pas devant tous les formulaires.
  • Et décidez volontairement de ce qui se passe quand le fournisseur ne répond pas, avec un délai d'attente maximal fixé par vous.

Où se trouve le code

Le paquet est sur Packagist, le code sur GitHub et la documentation du paquet rassemble la configuration des trois fournisseurs. Il est sous licence MIT, sans tables, sans routes et sans dépendances tierces : il utilise le client HTTP déjà fourni par Laravel.

Si vous avez des formulaires publics qui déclenchent des courriels ou des appels payants, c'est l'une des choses les moins chères que vous puissiez mettre en place et l'une de celles qui suppriment le plus de bruit. Nous travaillons cela dans applications web et conception web, et si vous nous racontez ce qui vous arrive nous vous dirons quelle couche vous manque.

Écrit par
Edu Lazaro
Edu Lazaro
Founder & Lead Developer en AndorraDev

Développeur full-stack avec plus de 15 ans d'expérience en Laravel, React, Node.js et architectures cloud. J'aide les entreprises en Andorre à construire leur présence digitale.

Partner de diseño · ionospace.
Besoin d'aide? ×
Andie by AndorraDev
Assistant IA + équipe humaine
Assistant IA d'AndorraDev
Andie
Bonjour! Je suis Andie, l'assistant IA d'AndorraDev. Comment puis-je vous aider? Si vous souhaitez parler avec Edu, il suffit de le demander.
17:30