Méthodes de protection contre le SPAM n° 2 : tout est une question de timing
Dans le cadre de la série « SPAM protection methods », nous continuons avec la partie 2 et le titre énigmatique « Le timing, c'est tout ». Elle repose sur l'idée que les utilisateurs qui remplissent un formulaire web ne l'ont pas rempli en entier en quelques secondes. Si l'on en croit l'omniscient Wikipédia, le nombre de frappes au clavier d'un assistant ou d'une assistante de bureau est d'environ 230 caractères par minute. Chez moi, un auto-test a donné 512 par minute (on peut le découvrir très simplement ici). Partons d'une valeur moyenne et disons que quiconque peut taper plus vite que 400 caractères par minute est très probablement un bot qui remplit le formulaire automatiquement. Allons-y.
1. Les maths (beurk)
Nous devons d'abord calculer combien de frappes par seconde sont permises si l'on part de 400 caractères par minute : 400 / 60 = 6,66666667 (donc 7). Plus de 7 frappes par seconde est donc à considérer avec méfiance. Pour obtenir la moyenne (on tape parfois plus vite, parfois plus lentement), disons en outre que quiconque fait plus de 35 (= 5x7) frappes en 5 secondes pourrait être un bot. Voilà pour les maths. Facile.
2. Le déroulement
Nous définissons un intervalle qui remet le compteur de frappes à zéro toutes les 5 secondes. À chaque « keypress », le compteur augmente de 1, et si nous dépassons 35, nous prenons des mesures adaptées contre le bot. Ces « mesures adaptées » consistent à envoyer une requête POST à captcha.php (pourquoi elle s'appelle ainsi, nous le verrons plus bas. Petit indice : elle contient un captcha) et à y enregistrer dans la session qu'il s'agit d'un bot. Pour faire simple, nous utilisons jQuery comme outil.
3. Le Javascript
var typecounter = 0;
setInterval(function(){
typecounter = 0;
}, 5000);
$(document).keypress(function(){
typecounter++;
if (typecounter > 35){
$.post("captcha.php", { action: 'block' }).done(function(data) {
if (data == 'block') {
location.reload();
}
});
}
});
Toutes les 5 secondes, le typecounter est remis à 0. À chaque keypress, il augmente de 1, et lorsqu'il atteint 35, nous rechargeons la page et affichons un message d'erreur.
4. Le PHP (captcha.php)
session_start();
$_SESSION['is_bot'] = true;
die('block');
captcha.php ne fait d'abord rien d'autre que de mettre la variable de session 'is_bot' à true et de renvoyer le message 'block'.
5. Rester dehors !
Pour que le bot ne puisse plus accéder à notre site (et reçoive aussitôt un message d'erreur), nous devons ajouter au début de chaque fichier à protéger les deux lignes suivantes :
session_start();
if (isset($_SESSION['is_bot']) && $_SESSION['is_bot'] === true) die("Kein Zugang für Bots");
Cela ne fait rien d'autre que de démarrer la session et de vérifier la valeur de la variable de session 'is_bot', que nous avons éventuellement déjà définie dans captcha.php. Si elle contient 'true', la sortie est interrompue et le message « Kein Zugang für Bots » (accès interdit aux bots) est affiché.
6. Optimisations
Il serait bien sûr optimal de demander un captcha au préalable. Peut-être s'agit-il malgré tout d'un humain qui tape simplement vite ? Allons-y donc.
7. Captcha
Nous ajoutons à notre fichier deux petites couches de division (alias DIV) et y écrivons un texte élégant. J'ai renoncé à externaliser les styles ou à les placer dans l'en-tête, pour plus de clarté.
<div id="captcha_wrapper" style="display:none; width:100vw; height:100vh; background-color:rgba(0,0,0,.8); align-items: center; justify-content: center;">
<div style="background-color:#fff; padding:1em;">
<h2>Bist du ein Mensch</h2>
Hoooman! Du tippst ja schnell - leider lässt das für mich nur den Schluss zu, dass du ein böser Bot bist. Daher bitte ich dich, folgendes einfaches Captcha zu lösen. Was siehst du für einen Text in dem Bild?
<img src='captcha.php' id='captcha_out'>
<input type="text" id="txtCaptcha">
<button id="checkCaptcha">Prüfen</button>
</div>
</div>
Dans le champ '#txtCaptcha', il faut saisir le captcha de l'image, et le bouton '#checkCaptcha' déclenchera plus tard la vérification de la valeur saisie.
8. Le Javascript pour le captcha
Bien sûr, nous modifions maintenant un peu le JavaScript. Au lieu de « location.reload », nous affichons maintenant le DIV du captcha.
var typecounter = 0;
setInterval(function(){
typecounter = 0;
}, 5000);
$(document).keypress(function(){
typecounter++;
if (typecounter > 35){
$.post("captcha.php", { action: 'block' }).done(function(data) {
if (data == 'block') {
$("#captcha_wrapper").css('display', 'flex');
}
});
}
});
En principe, c'est la même chose que plus haut, seule la ligne
$("#captcha_wrapper").css('display', 'flex');
est nouvelle. J'ai opté pour 'display: flex' afin que la boîte du captcha soit centrée.
9. Captcha.php
Le code PHP doit lui aussi être adapté. Nous bloquons l'utilisateur une première fois (d'abord temporairement), puis nous voulons renvoyer un captcha et le comparer lors d'une étape suivante.
session_start();
if (isset($_POST['action']) && $_POST['action'] == 'block'){
$_SESSION['blocked'] = true;
die('block');
}else if (isset($_POST['action']) && $_POST['action'] == 'check'){
if ($_POST['value'] == $_SESSION['captcha']){
$_SESSION['human'] = true;
$_SESSION['is_bot'] = false;
die('unblock');
}
}else{
header('Content-Type: image/png');
$width = 200;
$height = 30;
$im = imagecreatetruecolor($width, $height);
$white = imagecolorallocate($im, 255, 255, 255);
imagefilledrectangle($im, 0, 0, 399, 29, $white);
$characters = '123456789abcdefghijklmnpqrstuvwxyzABCDEFGHIJKLMNPQRSTUVWXYZ';
$randomString = '';
for ($i = 0; $i < 6; $i++) {
$char = $characters[rand(0, strlen($characters) - 1)];
imagestring($im, '2', 5+$i*20+rand(0, 10), 10, $char, imagecolorallocate($im, rand(0,100), rand(0, 100), rand(0, 100)));
$randomString .= $char;
}
$_SESSION['captcha'] = $randomString;
imagepng($im);
imagedestroy($im);
}
On peut expérimenter un peu avec ce code. J'ai choisi 6 lettres dans la plage [0-9a-Z] sans les lettres o, O et 0, pour éviter toute confusion chez les utilisateurs plus âgés :-)
10. Vérification du captcha
Enfin, nous vérifions la valeur du captcha. Pour cela, nous ajoutons le JavaScript suivant :
$("#checkCaptcha").click(function(){
$.post("captcha.php", { action: 'check', value: $('#txtCaptcha').val() }).done(function(data) {
if (data == 'unblock') {
$("#captcha_wrapper").css("display", 'none');
}else{
$("#captcha_out").attr("src", "captcha.php");
}
});
});
Au clic sur le bouton #checkCaptcha, nous postons ici d'une part l'action 'check' et d'autre part la chaîne saisie dans #txtCaptcha. Si nous recevons la valeur 'unblock', nous masquons de nouveau l'enveloppe du captcha et l'utilisateur peut continuer normalement. Sinon, un nouveau captcha s'affiche.
11. Vérification de la valeur
Enfin, nous modifions la vérification au début de chaque fichier protégé et contrôlons aussi que $_SESSION['human'] n'est pas true. Cela permet qu'un captcha déjà saisi fasse que les futurs « frappeurs trop rapides » ne soient plus considérés comme une activité de bot.
session_start();
session_destroy();
$_SESSION['is_bot'] = false;
if (isset($_SESSION['is_bot']) && $_SESSION['is_bot'] === true && !$_SESSION['human'] === true) die("Kein Zugang für Bots");
Le message « Kein Zugang für Bots » peut bien sûr être rendu un peu plus drôle, et qui veut vraiment être sûr peut redemander ici un captcha : l'utilisateur n'a peut-être pas saisi correctement le captcha lors de la première demande (ou n'a même pas essayé), a fermé le navigateur et veut maintenant retenter le captcha ; il devrait alors, en théorie, attendre l'expiration de la session (ou la suppression des cookies).
TL;DR
- L'utilisateur tape plus vite que 35 caractères en 5 secondes ? Peut-être un bot ?
- Afficher un DIV avec un captcha, et bloquer d'ores et déjà temporairement l'utilisateur via la session
- Demander le captcha : s'il est correct, définir la variable de session ; sinon, générer un nouveau captcha.