Base de connaissances / Knowledge base

Protection (en PHP) formulaire -> mail (31.07.08)
Un robot peut facilement utiliser un formulaire sur le web pour envoyer du spam ou autres comportements abusifs. Quelques pistes pour en éviter le maximum.

Exposé

De plus en plus de robots peuvent trouver votre formulaire (avec une simple recherche Google, par exemple name email address). Ils vont ensuite l'analyser et faire des submits depuis le robot directement, sans passer par la page de votre formulaire (donc ne retirer que les champs ou que le formulaire ne sert à rien; il faut modifier/retirer le script qui reçoit les champs, le action du formulaire). Il peux en résulter des actions catastrophiques si votre script de traitement de formulaire n'est pas assez robuste. Nous allons ici faire le tour de quelques techniques pour limiter les dégâts. Notez que les exemples sont en PHP mais que leurs principes sont applicables dans tout autre langage utilisé. Les techniques décrites ici sont complémentaires; l'utilisation d'une seule ne représente pas une protection assez efficace.

Referer

Chaque requête web envoyée au serveur peut avoir un champs 'Referer' (Dans le cas où il a demandé cette page depuis une autre page) ou pas (S'il a tapé l'URL dans la barre 'location' de son browser). Si un robot envoie des submits de formulaire, il n'y a pas le bon referer par défaut (mais le programmeur du robot peut l'avoir ajouté à la main à son script.). En début de traitement, on peut donc regarder ce referer et rejeter la requête s'il ne correspond pas à l'URL de la page de notre formulaire.

Soit un formulaire sur http://www.domain.tld/form.php avec comme tag de formulaire. Le script de traitement submit.php aura le test suivant:
if ($_SERVER["HTTP_REFERER"] != "http://www.domain.tld/form.php"){
   exit("Pas depuis notre site");
}

URLs

Les robots aiment bien poster des URLs dans vos formulaires, pour faire de la publicité pour un site que ce soit par mail ou sur votre site (Exemple: forum), voire en pensant quils vont augmenter leur ranking Google en ayant plein de liens vers leur site. En refusant simplement les URLs dans les champs, on empêche ces robots de faire leur travail:
if ((strstr($_GET["nom"], "http://")) or (strstr($_GET["message"], "http://")){
   exit("URLs pas acceptés");
}

Session

On place dans la homepage du site un bout de code qui set une variable de session:
$_SESSION["recom"] = "xx";

Sur la page du formulaire (ou de son traitement), le script vérifie que cette variable est juste. Si c'est faux, on retourne sur la homepage:
$recomcheck = $_SESSION["recom"];
if ($recomcheck != "xx"){
   header("Location: http://www.site.com/");
   exit();
}

De cette façon, on évite les clients ne pouvant pas enregistrer une session, soit notamment des robots.

Javascript et Champs cachés

NB: J'ai remplacé les 'plusGrandQue' et 'plusPetitQue' par des parenthèses car Zitem trash l'HTML à la 2e édition.

Le formulaire contient trois champs cachés dont un est rempli et les deux autres vides:
(input style="display:none;" type="text" name="comment1" id="comment1" size="1" value="!filled!")
(input style="display:none;" type="text" name="comment2" id="comment2" size="1" value="")
(input style="display:none;" type="text" name="comment3" id="comment3" size="1" value="")

Notons que ce sont de vrais champs de type 'text', pas 'hidden', mais qu'ils sont cachés par CSS.

Un bout de javascript modifie ensuite le second champ au moment du submit:
(form action ="./tell_friend.php" method="post" onsubmit="document.getElementById('comment2').value=document.getElementById('comment1').value;")

Au moment de recevoir les données, on vérifie:

   1. Que le premier champ contient toujours '!filled!'
   2. Que le second champs contient '!filled!'
   3. Que le troisième est toujours vide

De cette façon, on évite les robots qui, n'ayant pas javascript, rendront un second champ vide.
On évite aussi les robots qui remplissent tous les champs.

Enfin, on peut être plus imaginatif sur le nom des champs (Ex: appeler le 3e 'email') et sur le remplissage préalable du champ 1 (Ex: 'info@mondomaine.com')

Sauts de lignes

Lors du traitement, on vérifie qu'aucun champs (sauf les textarea s'il y en a) ne contienne de sauts de lignes, souvent utilisé pour envoyer des entête à sendmail:
$nom = trim($_POST["nom"]);
$prenom = trim($_POST["prenom"]);
$email = trim($_POST["email"]);
$emailto = trim($_POST["emailto"]);
if ((strstr($nom, "\n")) or (strstr($nom, "\r")) or (strstr($prenom, "\n")) or (strstr($prenom, "\r")) or (strstr($email, "\n")) or (strstr($email, "\r"))\ or (strstr($emailto, "\n")) or (strstr($emailto, "\r"))){
  exit();
}

On évite ici l'envoi d'en-têtes de mail.

Entêtes de mail

En complément au code précédent, vérifions qu'il n'y aie pas le moindre entête de mail dans tous les champs. Si c'est le cas, on remplace par un motif quelquonque, normalement introuvable dans un texte (Ici trois asterisques)
$find = array("/bcc\:/i","/Content\-Type\:/i","/cc\:/i","/to\:/i", "/Mime\-Type\:/i");
$email = preg_replace($find, "***", $email);
$nom = preg_replace($find, "***", $nom);
$message = preg_replace($find, "***", $message);

Puis on regarde si l'un des champs contient notre '***':
if ((strstr(
$email, "***")) or (strstr($nom, "***")) or (strstr($message, "***"))){
  exit("Pas de spammers.");
}


Adresses valides

On vérifie que la/les adresse email entrées sont valide dans leur format:
if ((!emailsyntax_is_valid($email)) or (!emailsyntax_is_valid($emailto))){
burp($mailerr[$langue]);
}

NB: la fonction emailsyntax_is_valid() est une fonction interne à l'environnement de développement de SysCo (subversion 'syscoPhpLib', fichier syscoPhpLib__functions__mail.php). La fonction est similiaire à la fonction reproduite ci-dessous:

function email_syntax_is_valid($email){
list($local, $domain) = explode("@", $email);
$pattern_local = '^([0-9a-z]*([-|_]?[0-9a-z]+)*)(([-|_]?)\.([-|_]?)[0-9a-z]*([-|_]?[0-9a-z]+)+)*([-|_]?)$';
$pattern_domain = '^([0-9a-z]+([-]?[0-9a-z]+)*)(([-]?)\.([-]?)[0-9a-z]*([-]?[0-9a-z]+)+)*\.[a-z]{2,4}$';
$match_local = eregi($pattern_local, $local);
$match_domain = eregi($pattern_domain, $domain);

if ($match_local && $match_domain){
  
return TRUE;
}
else{
  
return FALSE;
}

}


Play it again

Au tout début du traitement, on place un cookie -d'une durée de vie très courte, mettons 60 secondes:
if (isset($_POST["email"])){
  setcookie("recomSent", "1", time()+60);
}

Dans le script qui affiche le formulaire, on fait en sorte que, si le cookie existe, on envoie l'utilisateur sur une page d'explications (Ex: Pour des raisons anti-spam, il n'est pas possible d'envoyer ce formulaire plusieurs fois de suite dna sun temps court; merci de revenir plus tard.)

if ($_COOKIE["recomSent"] == "1"){
  header("Location: http://www.site.com/explication.php");
  exit();
}

De cette façon on évite l'envoi massif rapproché (type: 30 fois par minute). Après les 60 secondes, le cookie n'existe plus et le script ré-affichera le formulaire.

CAPTCHA

C.A.P.T.C.H.A. signifie Completely Automated Public Turing test to Tell Computers and Humans Apart, soit un test de Turing public entièrement automatisé pour différencier les ordinateurs des humains. Il s'agit généralement d'un ajout au formulaire, sous la forme de texte brouillé dans une image, d'une question un peu sotte ou de l'interprétation d'un fichier sonore ou vidéo.

Si la forme du texte brouillé dans une image est souvent rencontrée, elle pose de nombreux problèmes, notamment aux personnes ayant une mauvaise vue et aux personnes aveugles. Ces personnes seront donc exclues de votre site.

La question un peu sotte, c'est par exemple "Quelle est la couleur du cheval blanc de Napoléon?" ou des mathématiques simples comme "Combien font, en chiffres, 5 plus 2?". Dans tous les cas, l'idée est d'avoir une collection de questions/réponses et d'en proposer une tirée au hasard lors de chaque demande du formulaire. On stockera dans un fichier ou une base de données l'intitulé de la question, la réponse juste et un identifiant unique qui, inclus sous la forme d'un champs caché, permettra de vérifier la réponse.

Le fichier sonore peut être un simple MP3 qui dit un mot, mot qu'il faut ensuite ajouter dans un champs du formulaire.

Dans tous les cas, si le test échoue, on affichera un petit texte d'explication ainsi qu'une adresse email afin de ne pas frustrer l'utilisateur.

Conclusion

Les moyens exposés ici, si tous mis en oeuvre, vous assureront une bonne couverture contre le spam via formulaire web. Évidemment il ne permettent pas une protection à 100%. Cet article sera modifié à chaque fois qu'une nouvelle manière de polluer sera trouvée.