Sécuriser un formulaire sans captcha

by Frato on février 6, 2010

Eternel problème du développeur web, sécuriser les formulaires qui se trouvent sur un site pour empêcher les robots de soumettrent des formulaires de spam. Jusqu’à aujourd’hui, deux solutions s’offrent à nous pour bloquer ces robots :

  • Utiliser un captcha
  • Utiliser les services d’un filtre antispam, genre Akismet

Je suis personnellement totalement contre les captcha, qui compliquent beaucoup trop la saisie d’un formulaire, qui se doit d’être simple si on veut que le visiteur le remplisse et l’envoi. Les captcha affichent des images de plus en plus compliquées, au point que même un être humain a du mal à identifier les lettres qu’il est censé recopier.
Pour les filtres anti-spam, je n’ai pas pris le temps de tester, peut être est-ce efficace, mais cela fait reposer le bon fonctionnement de votre formulaire sur un service tiers, et ajoute donc des potentialités de pannes.

La solution que j’utilise était jusqu’à présent composée d’une double protection, qui s’est avérée insuffisante, je viens donc d’en ajouter une troisième.

1. Contrôler les champs renvoyés par le formulaire :

Il arrive que les robots ajoutent des champs au formulaire qu’ils renvoient. La solution consiste donc à ne traiter que les formulaires qui ne contiennent que les champs que nous avons défini.
Pour l’exemple, utilisons un formulaire de contact ou l’utilisateur entre son nom, son mail et un message. Le formulaire retournera donc 4 résultats : nom, mail, message, ainsi que la valeur du bouton submit (si le formulaire est envoyé avec un champ input submit et pas un lien).

En php, on définit une liste de champs acceptés :

$whitelist = array('nom', 'mail', 'message', 'envoyer');

Lors de la soumission du formulaire, on contrôle ce qui est retourné :

if(checkWhitelist($whitelist))
{
     // Traitment du formulaire
}

La fonction checkWhitelist retourne false s’il trouve un champ qui ne figure pas dans la whitelist

function checkWhitelist ($list)
{
	foreach ($_POST as $key => $item)
	{
		if (!in_array($key, $list))
		{
			return false;
		}
	}

	return true;
}

2. Placer un token en session

De nombreux robots ne sont pas capable de gérer des cookies. La solution consiste donc à placer un nombre aléatoire dans un champ caché du formulaire, et à stocker ce nombre en session. Une fois que le formulaire est retourné, on vérifie que le nombre retourné par le formulaire est identique au nombre stocké en session. Les robots ne gérant pas les sessions ne retourneront rien, et du coup le formulaire ne sera pas traité.

Au chargement de la page, on génère un token (le paramètre de la fonction est le nom du formulaire) :

$token = generateFormToken('contact_form');

La fonction generateFormToken génère un nombre aléatoire, le stock en session, et le retourne.

function generateFormToken($form)
{
    $token = md5(uniqid(microtime(), true));
    $_SESSION[$form.'_token'] = $token;
    return $token;
}

On place ensuite ce token dans un champ caché du formulaire :

<input type="hidden" name="token" value="<?php echo $token; ?>" />

Lorsque le formulaire est retourné, on vérifie le token :

if(verifyFormToken('contact_form'))
{
    // Traitement du formulaire
}

Fonction verifyFormToken :

function verifyFormToken($form)
{
    if (!isset($_SESSION[$form.'_token']))
    {
        return false;
    }

    if (!isset($_POST['token']))
    {
        return false;
    }

    if ($_SESSION[$form.'_token'] !== $_POST['token'])
    {
        return false;
    }

    return true;
}

3. Changer le contenu d’un champ caché en javascript

Les robots ne remplissent pas les formulaires, ils se contentent d’envoyer directement le contenu du formulaire à l’adresse définie. La troisième solution consiste donc à placer un champ caché dans le formulaire, avec une valeur de 0 (ou autre), et de changer la valeur de ce champ lorsque l’utilisateur saisie son nom (ou autre).

A l’aide de Jquery, on attache une action qui sera déclenchée dès que l’utilisateur placera son curseur dans le champ nom. Cette action va modifier la valeur de notre champ caché (appellé ici « control ») :

$(function(){
	$('#nom').bind('focus', function(){
		$("#control").val('666');
	})
});

Lorsque le formulaire est renvoyé, on vérifie que le champ control a bien la valeur 666 :

if($_POST['control'] == 666)
{
    // Traitement du formulaire
}

Pour résumer, voici le script en entier :

<?php
	session_start();

	function generateFormToken($form)
	{

	    $token = md5(uniqid(microtime(), true));
		$_SESSION[$form.'_token'] = $token;
	   	return $token;
	}

	function verifyFormToken($form)
	{

	    if (!isset($_SESSION[$form.'_token']))
		{
			return false;
	    }

		if (!isset($_POST['token']))
		{
			return false;
	    }

		if ($_SESSION[$form.'_token'] !== $_POST['token'])
		{
			return false;
	    }

		return true;
	}

	function checkWhitelist ($list)
	{
		foreach ($_POST as $key => $item)
		{
			if (!in_array($key, $list))
			{
				return false;
			}
		}

		return true;
	}

	$whitelist = array('nom', 'mail', 'message', 'envoyer', 'control', 'token');

	if(isset($_POST['nom']) && verifyFormToken('contact_form') && checkWhitelist($whitelist) && $_POST['control'] == 666)
	{
		// Traitement du formulaire
	}

	$token = generateFormToken('contact_form');
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
	<title></title>
	<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4/jquery.min.js"></script>
	<script type="text/javascript">
		$(function(){
			$('#mail').bind('focus', function(){
				$("#control").val('666');
			});
		});
	</script>
</head>
<body>
	<form action="" method="post" name="contact_form">
		<label for="nom">Nom</label>
		<input type="text" name="nom" id="nom" />
		<label for="mail">Mail</label>
		<input type="text" name="mail" id="mail" />
		<label for="message">Message</label>
		<textarea name="message"></textarea>
		<input type="hidden" name="control" id="control" value="0" />
		<input type="hidden" name="token" value="<?php echo $token; ?>" />
		<input type="submit" name="envoyer" value="Envoyer" />
	</form>
</body>
</html>

J’utilise cette solution depuis quelques temps, elle semble efficace. Concernant les deux premières techniques, je les ai trouvé sur un site anglais, mais pas moyen de retrouver l’adresse de ce site ; je l’ajouterai si je la retrouve. Pour la troisième technique, elle pose un problème si le visiteur du site a désactivé le javascript sur son navigateur. A vous de voir si cela est acceptable ou pas.

Si vous utilisez une autre technique, qui soit simple et efficace, n’hésitez pas à en faire part dans les commentaires.

19 comments

Je ne suis pas développeur web mais j’ai parfois constaté sur certains site une astuce toute simple pour contourner les robots : poser une question comme « de quelle couleur est le cheval blanc d’Henri IV ? » « qu’est-ce qui brule, le feu ou l’eau ? » etc.

C’est peut-être facile à contourner si on programme la réponse qui correspond, mais avec un roulement de question dans ce genre n’y aurait-il pas moyen d’éviter astucieusement ces illisibles captcha ? ^^

by Emmanuel on 09/02/2010 at 8:40 . #

Oui, c’est une solution efficace, mais selon les sites et les types de publics, on ne peut pas poser de « question con » :)

by Frato on 10/02/2010 at 8:19 . #

Bonsoir,

Comment on configure notre mail dans ce formulaire svp ?

by Fred on 13/02/2010 at 1:55 . #

Bonjour FRED,

Entre les acolades lignes 49 et 51 :)
Pour faire son propre traitement lire la doc de PHP http://php.net/manual/fr/function.mail.php

{
// Traitement du formulaire

     $to      = 'mail@maildeFRED.com';
     $subject = 'le sujet';
     $textnom = 'Nom : '.$nom.'\n';
     $textmail = 'Mail : '.$mail.'\n';
     $textmessage = 'Message : '.$message.'\n';
     $headers = 'From: webmaster@example.com' . "\r\n" .
     'Reply-To: webmaster@example.com' . "\r\n" .
     'X-Mailer: PHP/' . phpversion();

     mail($to, $subject, $textnom, $textmail, $textmessage, $headers);
}

squirrel

by squirrel on 28/04/2010 at 3:12 . #

Il me semble que cet article peu ajouter des vérifications de plus, la technique du cookie sur 60 secondes est pas mal, elle perment d’éviter les multiples postes à répétitions.

http://www.zhotspot.net/website/index.php/Protection-en-PHP-formulaire-%3E-mail/

squirrel :)

by squirrel on 17/05/2010 at 3:46 . #

bonjour

oui ton alternative au captcha est interressante
merci pour ce petit cours

a++

by jc on 06/03/2011 at 12:05 . #

Salut,

Super le coup du token, je l’ai testé sur un site, plus de spam :)

Me viens une autre méthode :

4. Vérifier le domaine du HTTP_REFERER, doit être son propre domaine.

Serait-ce efficace, ou est-ce falsifiable ?

by jhice on 14/03/2011 at 11:07 . #

Il reste le problème des spammeurs qui font une requête pour obtenir le formulaire (obtention du token) et qui execute le javascript avant de poster la réponse.

by Web Imago on 14/02/2012 at 12:25 . #

Après avoir déployé cette solution sur de nombreux sites, plusieurs d’entre eux recevant plusieurs dizaines de spam de formulaires quotidien, je peux t’assurer que ça fonctionne très (très) bien. Je n’ai pas encore trouvé plus fiable.

by Frato on 14/02/2012 at 12:52 . #

Bonjour et merci pour cette solution. J’avais déjà fait un truc comme ça et voilà qui confirme que c’est bien la bonne solution. C’est simple autant à la mise en oeuvre qu’à l’utilisation.

Il y a aussi une méthode super simple qui est d’ajouter un champs type texte mais caché en CSS avec ou sans valeur par défaut.
Les robots ne pouvant s’empêcher de remplir tout type de champs texte sans même se soucier de l’affichage, il se grille de lui-même au traitement de données s’il a rempli ou changé la valeur de ce champs graphiquement caché.

Un tel système, tout comme celui qui est présenté ici, peut aussi servir à compléter une blacklist de domaines susceptibles d’appartenir à des spammeurs et peut-être une autre blacklist d’IP

En tout cas, je garde cette page sous le coude.
Merci ;)

by Vidda on 25/02/2012 at 12:30 . #

Superbe !
j’ai réussi à le mettre en place localement avec un petit formulaire basique (alors que je n’y connais rien question code, ça marche : miracle ! mais, là où ça se complique c’est avec le traitement du formulaire qui est effectué par ce script :
***********************************
// Define some constants
define( « RECIPIENT_NAME », « expediteur » );
define( « RECIPIENT_EMAIL », « receptionnaire@domaine.fr » );
define( « EMAIL_SUBJECT », « Un message en provenance du formulaire de contact » );

// Read the form values
$success = false;
$senderName = isset( $_POST['senderName'] ) ? preg_replace( « /[^\.\-\' a-zA-Z0-9]/ », «  », $_POST['senderName'] ) : «  »;
$senderEmail = isset( $_POST['senderEmail'] ) ? preg_replace( « /[^\.\-\_\@a-zA-Z0-9]/ », «  », $_POST['senderEmail'] ) : «  »;
$message = isset( $_POST['message'] ) ? preg_replace( « /(From:|To:|BCC:|CC:|Subject:|Content-Type:)/ », «  », $_POST['message'] ) : «  »;

// If all values exist, send the email

if ( $senderName && $senderEmail && $message ) {
$recipient = RECIPIENT_NAME .  » « ;
$headers = « From:  » . $senderName .  » « ;
$success = mail( $recipient, EMAIL_SUBJECT, $message, $headers );
}

// Return an appropriate response to the browser
if ( isset($_GET["ajax"]) ) {
echo $success ? « success » : « error »;
} else {
?>

Merci

<?php if ( $success ) echo "Merci pour votre message ! Je vous réponds dès que possible. » ?>
<?php if ( !$success ) echo "Il y a eu un problème à l’envoi de votre message. Merci de réessayer. » ?>
Cliquer sur le bouton de retour dans votre navigateur pour revenir sur la page.

<?php
}
******************************
C’est bien sûr là :
if(isset($_POST['nom']) && verifyFormToken('contact_form') && checkWhitelist($whitelist) && $_POST['control'] == 666)
{
// Traitement du formulaire
}

$token = generateFormToken('contact_form');
que mon problème se pose.

Comme c’est pour un copain qui est encore plus incompétent que moi (c’est peu dire) et qui est envahi de spams j’ai bien envie de lui mettre ta solution,
mais j’ai besoin d’un petit coup de main.
A votre bon coeur. ;)

by André on 08/10/2012 at 3:20 . #

Héhé! Ca promet!
Je me demandais (un peu parano) :
Dans le traitement du formulaire, le bot lit le code source et voit : mail@maildefred.com !!! Est-ce que je me trompe.

Dans un souci de tout compliquer, j’aurais souhaité combiner cette vérification en PHP, et ce travestissement en js :

function victor_hugo()
{
maupassant=new String(« adresseemileajardomainejeanbaptistepoquelinprout »);
gary= »emileajar »;
moliere= »jeanbaptistepoquelin »;
maupassant=maupassant.split(gary);
maupassant[0]+= »@ »;
maupassant=maupassant[0].concat(maupassant[1]);
maupassant=maupassant.split(moliere);
maupassant[0]+= ». »;
maupassant=maupassant[0].concat(maupassant[1]);

return maupassant;
}
document.write(victor_hugo());

Mais je suis perdu : comment faire pour que, quand on clique sur « envoyer », le formulaire soit vérifié et l’adresse démasquée?

by As on 22/11/2013 at 4:38 . #

bonjour

on peut aussi écrire un fichier texte vide dans un dossier protégé par un htaccess , ça évite session ou cookie
on garde le méme fonctionnement , on vérifie simplement si le fichier existe

by bruno on 04/12/2013 at 11:44 . #

La partie qui gère l’envoi du formulaire est en php, traité coté serveur, le bot n’y a pas accès, l’adresse mail n’est jamais visible.

by Frato on 04/12/2013 at 12:07 . #

Merci! C’est élémentaire, encore fallait-il le savoir!
Du coup j’ai suivi scrupuleusement le procédé : je comprends à peu près ce que j’écris, mais les résultats me surprennent :

- En testant sur Wampserver, c’est nickel, je remplis le formulaire, et je reçois direct un mail!

- Sur mon site hébergé chez OVH, la première tentative est toujours infructueuse : il faut remplir une seconde fois le formulaire pour que le mail soit envoyé (ou du moins reçu!). Il faut que la page soit au moins une fois rechargée, en quelque sorte.
A votre avis, est-ce un problème de serveur ou de php? Pourquoi Wampserver ne donne-t-il pas le même résultat que le serveur d’OVH pour ce même code php?

by As on 08/02/2014 at 4:36 . #

@As n’aurais-tu pas un problème à cause du phpsessid d’OVH ? Tu peux essayer en ajoutant cette ligne dans ton .htaccess :

SetEnv SESSION_USE_TRANS_SID 0

by Frato on 08/02/2014 at 5:08 . #

Mais c’est pas vrai!!
C’est nickel, merci!
Bon, il me reste quand même 2-3 trucs à apprendre sur le php…

by As on 09/02/2014 at 11:04 . #

bonjour,
comment être sur que sa fonctionne ?
Y’a til moyen de faire un test comme si on était un robot ou je ne sais quoi ?

by Fabrice on 27/02/2014 at 12:02 . #

Le meilleur moyen de savoir si cela fonctionne est de voir si tu reçois du spam via les formulaires ou pas.
Si tu veux voir les formulaires qui sont bloqués, tu peux modifier le script pour envoyer un mail différent pour les formulaires qui ne passent pas le test, tu vas recevoir plein de spam :)

by Frato on 27/02/2014 at 12:46 . #

Leave your comment

Required.

Required. Not published.

If you have one.