Débarasser drupal du SPAM sans captchas
Le 25 octobre 2007 à 01:49.

Les spam s sont une véritable infection, ce n'est un secret pour personne. Et les solutions pour lutter contre ce fléau sont aussi nombreuses que moyennement satisfaisantes. Il peut s'agir de déléguer ce travail à un service dédié (ex. akismet ), de les traiter à la main (ne riez pas, je l'ai fait pendant un temps Wink ) ou encore d'utiliser les fameux captchas, ces images illisibles qui sont d'une utilisation extrêmement désagréables pour les contributeurs.

Ce que je vous propose ici c'est une implémentation avec Drupal qui n'a rien de révolutionnaire tout en étant très efficace. L'idée étant de vérifier que les commentaires (et tous les formulaires en fait) sont en toute probabilité écrits par un humain.

Observations des nuisibles

Le spammeur va vite, très vite même. La plupart du temps il travaille en deux temps. Un premier de reconnaissance analyse vos pages. Soit il cherche à savoir quel plate-forme vous utilisez (drupal, wordpress, etc.) soit il analyse en aveugle les formulaires à la recherche de celui des commentaires. A ce titre il travaille un peu comme GoogleBot. Au passage, dans la série des légendes, contrairement à Google les scripts ne l'arrêtent pas. En tout cas ne les arrêtes pas tous, loin de là. Cacher un formulaire par un script ne fonctionne juste pas sauf à le mettre dans un fichier séparé, solution difficile à mettre en oeuvre sur la forme d'un plug-in Drupal. J'en ai même trouvé un qui n'était pas arrêté par les "boites de dialogues" javascript, les simulant allègrement...

Ensuite, une fois que votre site a été proprement analysé, il est mis en base de donnée. A partir de là, votre profile est utilisé à l'infini pour expédier des contenus de formulaire sous la forme d'une requête POST forgée proprement. Si la requête ne marche plus (formulaire modifié par exemple), le spammeur (ou plutôt son robot) va ré-analyse votre site et recommencer son manège.

Finalement la seule chose qui bloquerait proprement notre spammeur c'est le temps. Car soit il analyse (requête GET) et poste un commentaire (requête POST), soit il a déjà analysé et mémorisé le formulaire et ne fait plus que poster. Dans les deux cas, vu de votre serveur, une personne soumet un formulaire (POST) soit dans un temps très long, soit très court après un GET. L'idée de notre anti-spam maison est donc de stocker dans le formulaire une valeur de temps en secondes. Puis lors du post de comparer cette valeur avec celle de l'heure courante. Si le temps est inférieur à 10 secondes, c'est sûrement un spam (ou un utilisateur qui tape plus vite que son ombre Wink ).

Un module drupal

Pour implémenter cette fonction dans Drupal, il va nous falloir créer un module spécifique. Il y a déjà pas mal de littérature sur ce sujet je ne vais donc pas trop m'étendre là dessus. Un module drupal est un simple dossier portant le nom du module, ici vilains_spammeurs. Ce dossier contient deux fichiers, l'un d'information nommé vilains_spammeurs.info qui contiendra tout simplement :

name = "Anti Vilains Spammeurs"
description = "Module Anti Vilains Spammeurs"
package = "spam"
version = "5.x-1.0"
project = "vilains_spammeurs"

Et au même endroit, un fichier vilains_spammeurs.module qui contiendra pour l'instant

function vilains_spammeurs_help($section)
{
  switch ($section)
  {
    case 'admin/modules#description' :
      return t('Module anti vilains spammeurs.');
  }
}

Ceci fait, il suffit d'aller dans votre section administration/site building/modules pour activer ce simplissime plugin qui pour l'instant ne fait rien. Mais pas pour longtemps.

Le magic cookie

La première chose à faire est donc de stocker la date, ou plus exactement le nombre de secondes depuis 1970 dans un champ caché de tous les formulaires. En effet, tant qu'à faire, autant tout protéger dans la mesure où ne ciblons que les utilisateurs anonymes. Cela ne touche du coup que les commentaires et les créations de comptes.

Drupal fonctionne par "hook", c'est à dire que chaque module peut être étendu en fonctionnalité par d'autres modules sous réserve que ces derniers déclarent les bonnes méthodes. Par exemple, le module de "form", avant d'afficher un formulaire, va chercher dans tous les modules une méthode qui se nomme nomDuModule_alter_form. Cette méthode permet de modifier un formulaire. Donc si nous ajoutons à notre module une méthode vilains_spammeurs_alter_form, elle sera donc appelée en nous donnant l'opportunité de rajouter notre champ caché.

  function vilains_spammeurs_form_alter($form_id, &$form)
  {
  if ($user->uid==0 && variable_get($form_id . '_spam_filtered'))
  {
    $time_stamp=time();
    $fieldId=md5(getenv('REMOTE_ADDR'));
    error_log('ALTER - '.$_GET['q'].'/'.$form_id.'  = '.$fieldId.'='.$time_stamp);
    $form[$fieldId] = array(
    '#type' => 'hidden',
    '#default_value' => $time_stamp);
    $form['#submit']=array_merge(array('karmaLab_common_spam_validator'=>array()),$form['#submit']);
          $GLOBALS['conf']['cache'] = FALSE;
    return;
  }
 }

La méthode est assez simple. Tout d'abord on ne modifie que les formulaires des utilisateurs anonymes ($user->uid=0). On ne modifie pas non plus tous les formulaires. C'est le rôle du variable_get($form_id . '_spam_filtered') qui renvois un booléen en fonction de l'id du formulaire et que nous allons gérer un peu plus loin. Ensuite, petite astuce, on intercale notre propre méthode (vilains_spammeurs_validator) de validation dans la chaîne des validateurs de ce formulaire. Enfin, on ajoute un champ caché contenant le nombre de secondes depuis 1970 (fonction time() ). Autre astuce, le nom du champ est variable, il s'agit d'un HASH MD5 de l'adresse IP du client, rendant impossible le rejoue d'une requête mémorisée sur une autre machine que celle qui as affiché ce formulaire.

Lorsqu'un formulaire est altéré pour y insérer le champs caché, la procédure déconnecte du même coup le cache de la page (la commande avant return). La raison en est que sinon tous les utilisateurs anonymes auront le même timestamp que le premier utilisateur anonyme qui s'est connecté, vu que le formulaire sera en cache. N'activez (voir les réglages plus loin) donc la protection anti-spam QUE pour les formulaires vitaux (basiquement commentaires et login). Si par exemple vous activez cela pour la recherche et que vous avez un bloc de recherche sur toutes les pages, le cache sera complètement désactivé.
N'oubliez pas que cette méthode est appelée par drupal à la fabrication du formulaire mais aussi à la réception des données du formulaire. Vous noterz du coup l'utilisation de l'option #default_value (et non pas simplement #value) pour le champ magique de notre de formulaire. En effet, en retour (POST), si un robot a forgé "bêtement" une requête en omettant ce champ, sa valeur prendra automatiquement la valeur par défaut qui est ... l'heure courante. Cela correspondra donc automatiquement à un SPAM lors de l'analyse qui va suivre car le temps de réponse sera en toute logique de 0s. C'est un aspect important si vous portez cette méthode sur d'autres plate-formes que Drupal car cela évite au système d'assimiler une absence de champ à un temps trop long, et donc bon.

Deuxième étape, la validation du formulaire. Il va falloir ajouter à notre module la fonction vilains_spammeurs_validator que nous avons déclaré plus haut.

define("MIN_RESPONSE_TIME",10);
function vilains_spammeurs_validator($form_id, $form_values)
{
  $fieldId=md5(getenv('REMOTE_ADDR'));
  $timeStamp=$form_values[$fieldId];
  $currentTime=time();
  $responseTime = $currentTime-$timeStamp;
  $delay=variable_get($form_id.'_spam_delay',10);
  $failed  = $responseTime<$delay;
  error_log('SUBMIT - '.($failed?'SPAM':'OK').' - '.$_GET['q'].'/'.$form_id.' : '.$fieldId.' = '.$responseTime."s/".$delay." = [".$currentTime."-".$timeStamp."]");

  if ($failed)
  {
    error_log('Spam Content : '.$form_values['comment']);
    drupal_set_header('HTTP/1.1 666 Forbiden to spamers.');  
    print"Votre réponse est bien trop rapide, revenez en arrière et re-validez votre commentaire.
          S'il vous pensez qu'il s'agit d'un erreur, merci de m'envoyer un courriel à (en enlevant de truc_)"
;
    exit ();   
  }
  return;
}

Là aussi rien de très compliqué. Comme cette procédure n'est appelée que si le formulaire a été modifié par la procédure précédente, et qu'il n'a été modifié que si l'utilisateur est anonyme, pas la peine de revérifier que nous somme dans le cas "anonyme". Son travail se borne donc juste à extraire le temps d'origine du formulaire, à recréer notre nom de champ magique à partir du MD5 de l'IP client et de faire une différence avec le temps courant. Le define juste avant la procédure définit le temps minimum pour considérer que nous avons affaire à un spam. Ce temps minimum est récupéré d'une variable d'environnement Drupal en fonction de l'ID du formulaire. Nous verrons plus loin le réglage. La variable $failed prends donc la valeur true si ce temps est en dessous de celui défini. Sa valeur est testée par la condition qui suit et qui affiche ainsi le message d'avertissement.

Pour archives, nous mettons d'abord dans les logs les caractéristiques du SPAM. Ensuite nous fabriquons une mini-page avec un code d'erreur bidon (ici 666) et un texte d'explication. Du coup, s'il s'agit d'une erreur (utilisateur trop véloce), il suffit de revenir en arrière et de revalider pour que le commentaire passe. Une fois le message affiché, nous faisons un très brutale appel à exit() qui romps ainsi la chaîne de traitement du formulaire et empêche sa sauvegarde. Fin de l'histoire.

Le corps du message peut être amélioré avec un style et pourquoi pas un bouton de retour en arrière automatique en cas d'erreur (utilisateur trop rapide).

Il manque cependant encore un détail, le paramétrage de notre modules. Il faut en effet pouvoir changer les valeurs XXX_spam_filtered et XXX_spam_delay. Pour cela, nous utiliserons un paramétrage simple, sans base de données :

function vilains_spammeurs_settings()
{
  $form_ids= array (
    'comment_form' => t('Comment Form'),
    'user_login' => t('User Login Form'),
    'user_login_block' => t('User Login Form Block'),
    'user_edit' => t('User Edit Form'),
    'user_register' => t('User Registration Form'),
    'user_pass' => t('User Forgot Password Form'),
    'contact_mail_user' => t('User Contact Form'),
    'contact_mail_page' => t('Sitewide Contact Form'),
    'node_form' => t('Create a node'),
    'search_form' => t('Recherche'),);

  foreach ($form_ids as $form_id => $description)
  {
    $varname= $form_id . '_spam_filtered';
    $form[$varname]= array (
      '#type' => 'checkbox',
      '#title' => t("Filtrage de $description"),
      '#default_value' => variable_get($varname,0)
    );
    $varname= $form_id . '_spam_delay';
    $form[$varname]= array (
      '#type' => 'textfield',
      '#title' => t("Délai minimum"),
      '#default_value' => variable_get($varname,10)
    );
  }

  return system_settings_form($form);
}

Conclusion

Voilà, comme je le disais, c'est simple et efficace. Cela ne demande pas de javascript côté client et cela prend peu de ressources. Je n'ai pas de doute quant au fait que cette technique soit un jour contournée mais pour l'instant, cela bloque 100% de mes spams.

Pour ceux que cela intéresse, les sources sont disponibles ici sur le dépôt subversion : http://artisan.karma-lab.net/software/subversionspam_killer/tags/v0.1/spam_killer

Commentaires

Ulhume, le 25 October, 2007 - 12:38

50 spams tentés depuis l'écriture de cet article, il semble que la protection fonction Smiling

Pti-seb , le 25 October, 2007 - 13:45

J'ai tenté la mise en place d'un atni-spam fait maison, j'en suis arrivé à la conclusion suivante, les robots spammeurs ne font pas que envoyé un POST ou un GET, il sont capable d'ouvrir un navigateur, aller sur le site et poster un commentaire comme le ferait une vrai personne.

J'avais déjà vu un truc comme ça sous windows à une époque, on fesait une action à la souris, genre on clique sur une icône du bureau et cela génrère un script.

Si on rejoue le script, on voit la souris refaire la même chose toute seule.

Je vais implémenter ta méthode sous dotclear, je te redis si cela foncitonne. En tous les cas, si elle fonctionne, je ne peux m'empêcher de penser : pour combien de temps ?

Pti-seb , le 25 October, 2007 - 13:50

Au faite, pour consolider ta méthode, il faudrait mieux faire un hash md5 ou sha de la valeur du temps + ip de l'utilisateur + chaîne password. Sinon un robots spammeur peut simplement envoyer la valeur du temps qu'il souhaite pour déjouer le système.

Ulhume, le 25 October, 2007 - 16:02

Pour répondre à ton premier post, ça va bien dans le sens de ce que j'ai constaté. Les scripts sont bien joués comme le reste. Si effectivement il s'agit d'une automatisation d'un IE (ce qui est en tout cas souvent indiqué dans les signatures) comme tu l'indiques cela prend du sens. Au début j'avais tenté la méthode d'offuscation des formulaires et les remplaçant à la volé par un javascript à base de document.write et de liste de code UTF-8. Et les spams ont continué à affluer. J'ai donc abandonné cette méthode qui était pourtant élégante.

Pour ce qui est du moment où les bots de spam pour comprendre l'astuce, je me doute qu'il arrivera un jour ou l'autre. Mais pour y arriver, les deux seules solutions sont pour eux sont :
1/ S'il s'agit d'un vrai robot (pas d'un navigateur automatisé), de renvoyer le bon timestamp (genre ce que l'on insère + 10). Mais là cela leur demande de faire du code spécifique. Et on peu parer ce coup en mettant le nom du champ timestamp en settigs dans Drupal (ou dans ton CMS). Et en le générant aléatoirement à la première utilisation. Ainsi il va être difficile pour eux de faire une parade à la méthode en elle-même.
2/ S'il s'agit d'un "navigateur scripté" (genre un gecko ou un mshtml), il devra temporiser comme un humain leur réponse. C'est parfaitement imaginable en lançant un nombre N de GET à la suites, puis en reparcourant la liste depuis de début en faisant des POST. Mais l'inconvénient est qu'ils vont alors devoir lancer énormément d'instances en parallèle pour "remplir" ces 10 secondes. Sinon, cela va leur faire perdre du temps.

Ceci dit, pour l'instant, lorsque je regarde les logs, il semble que ce soit tous des 0s, c'est à dire des POST qui renvois la valeur timestamp à l'identique. Et si je regarde les logs je n'ai AUCUN GET au moment des faits. Pourtant il n'a pas pu inventer cette valeur. Là j'avoue que c'est un mystère pour moi.
La seule requête suffisamment proche dans le temps est un autre POST qui a aussi été débouté. Donc rien qui lui permette de récupérer les données du formulaire.

Maintenant le problème du hash c'est qu'il devient impossible de faire un delta pour évaluer le temps de réponse (ou alors j'ai mal compris). Et en plus le système est déconnecté en cas d'utilisateur authentifié (il n'y a plus de spam dans ce cas). En revanche, je retiens ton idée de l'IP car une chose est sure, depuis deux jours, je ne reçoit QUE des POST et plus de GET, si je compare IP à IP. Comme si l'enregistrement avait été ou est fait sur une machine donnée, et les POST sur d'autres. Donc encapsuler un hash de l'IP peut être un bon moyen aussi. A tester Smiling

Pti-seb , le 25 October, 2007 - 16:10

Bon ben j'ai mis en place la méthode, et j'ai toujours du spam à passer. Je laisse quand même le code car j'ai l'impression qu'il filtre un peu plus quand même (9 spams en 2heures).

Pour le hash md5 c'est pas possible, j'ai vue cela après, je suis donc partie sur une solution de cryptage/décryptage (cf. code source mes formulaire de post) qui fonctionne très bien.

PS : Sont coriace ces robots...

Ulhume, le 25 October, 2007 - 16:28

Hum.. Et qu'est-ce que tu as dans tes logs ? Quel est la différence de temps pour les spams qui passent ?

Ulhume, le 25 October, 2007 - 16:44

Bon, pour être sur d'être dans un cas normal, j'ai remis les url de commentaires aux normes drupal (comment/reply), comme cela je vais voir si j'ai moi aussi encore du spam qui passe.

Ulhume, le 25 October, 2007 - 18:04

Bon, j'ai compris le coup du 0s. En fait mon test de borne max ne servait à rien car l'utilisation du paramètre #default_value impliquait que même si le client n'avait pas posté ce champ, il était mis à la valeur par défaut de l'heure courante. Du coup pas de champs => time_stamp=heure_courante => 0s de temps de réponse => SPAM.

J'ai aussi repris l'idée de Pti-Seb pour intégrer l'IP client dans la méthode en l'utilisant, sous la forme d'un hash MD5 comme nom de champ. Du coup, si le spammeur utilise une autre IP et poste une requête pré-forgée, il tombe dans le cas de figure précédent.

Voilà, en espérant que cela serve.

Bast , le 18 December, 2007 - 17:44

L'adresse fournie pour le dépôt subversion ne mène nul part Wink

http://artisan.karma-lab.net/subversion/drupal/modules/spam_killer/tags/...

Ulhume, le 24 December, 2007 - 17:33

Euh.. Tu peux reverifier car je viens de tester et j'arrive bien sur le contenu du module...

Poster un nouveau commentaire

Le contenu de ce champ est gardé secret et ne sera pas montré publiquement.
  • Allowed HTML tags: <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd>
  • To highlight piece of code, just surround them with <code type="language"> Your code &tl;/code>>. Language can be java,c++,bash,etc... Everything Geshi support.
  • Les lignes et les paragraphes vont à la ligne automatiquement.
  • Textual smileys will be replaced with graphical ones.
  • Les adresses de pages web et de messagerie électronique sont transformées en liens automatiquement.

Plus d'informations sur les options de formatage

Connexion utilisateur
Les derniers bavardages...