Formulaire PHP anti-spam (captcha)

Les temps sont durs et le spam est devenu la vraie chienlit du net. Ayant dernièrement souffert des attaquent nombreuses sur mes formulaires, via des robots sans vergogne, j'ai vite décidé de développer une protection. Un captcha. Ce n'est pas nouveau car en place sur de nombreux sites (notamment les inscriptions aux forums) mais c'est juste une façon de voir la chose...

Un seul fichier : le formulaire récursif

Ce script utilise les session PHP, et la gestion classique de formulaires. Il faut lui ajouter un répertoire contenant des images/caractères, petit fichiers images au format GIF, contenant autant de caractères alphanumériques que souhaités. En voici sept. Le script vérifie la présence de ce répertoire et implémente automatiquement la série codée via le tableau $alphabet.

Une session $_SESSION['code'] garde en mémoire le code composé aléatoirement, à partir des noms de fichiers (a.gif, b.gif, 2.gif, etc.) sans extension, et une série $_SESSION['img_1'], $_SESSION['img_2'], etc. qui mémorise autant de noms de fichiers que $nbrchars le demande, afin de composer l'image $imagecode ensuite affichée.

La variable $erreur arrête le traitement du formulaire et permet l'affichage du type d'erreur rencontrée. Sinon, le formulaire redirige le visiteur vers une page $confirmation (index.php ira bien) qui affichera un message de confirmation grâce à au drapeau $_GET['mailok']. A noter qu'en cas de traitement récursif de la confirmation (auquel cas l'affichage du formulaire HTML doit être conditionnel) la session $_SESSION['code'] étant vidée, il est impossible de multiplier son envoi en rechargeant la page : le code toujours présent dans la variable $_POST['vateuf'] est différent de celui de la nouvelle session ! Une pierre, deux coups :-)

Fichier contact.php

<?php
#
# Editer ces variables
#
# répertoire des images sous la forme a.gif, b.gif, 1.gif, etc.
$rep_lettres = "netAlbum2/imgsys/lettres/";
# nombre de caractères (repris dans maxlength)
$nbrchars = 4;
# fichier de redirection, affichant la confirmation
$confirmation = "./";
#
# fin de l'édition
#
session_start();
$imagecode = "";
$erreur = ""; # toutes erreurs formulaire

# le formulaire a été envoyé on vérifie la correspondance
if($_POST && $_POST['vateuf'] != $_SESSION['code']) {
	$erreur = "Les caractères ne correspondent pas !";
	$_POST['vateuf'] = "";
}

# pas de session code ou demande d'un nouveau code
# implémentation des sessions (si le répertoire existe)
if((!$_SESSION['code'] || $_GET['newcode']) && $dossier = @opendir($rep_lettres)) {
	# si un nouveau code a été demandé on efface la session
	if($_GET['newcode']) $_SESSION['code'] = "";
	while ($fichier = @readdir($dossier)) {
		if( $fichier == "." || $fichier == ".." || is_dir($fichier) ) continue;
		# on implémente le tableau $alphabet des fichiers/caractères
		$alphabet[] = $fichier;
	}
	@closedir($dossier);
	$nbrimg = count($alphabet)-1;
	# tirage au sort des $nbrchars caractères composant le code
	for($i=0; $i<$nbrchars; $i++)
		$lettre[] = rand(0,$nbrimg);
	# implémentation de l'image $imagecode affichée,
	# de la session 'code' contenant la série de caractères à comparer
	# et des $nbrchars sessions préfixées par 'img_'
	$i=0;
	foreach($lettre as $val) {
		$imagecode .= "<img src='$rep_lettres$alphabet[$val]' alt=''>";
		$_SESSION['code'] .= basename($alphabet[$val],'.gif');
		$_SESSION['img_'.$i] = $alphabet[$val];
		$i++;
	}
# composition de l'image $imagecode si la session est implémentée
} elseif($_SESSION['code']) {
	for($i=0; $i<$nbrchars; $i++)
		$imagecode .= "<img src='".$rep_lettres.$_SESSION['img_'.$i]."' alt=''>";
}

if($_POST && !strlen($erreur)) {

	# pas d'erreur de code : traitement des éléments de formulaire...
	
	# ... une fois tout vérifié et mail envoyé on efface la session
	$_SESSION['code'] = "";
	# redirection vers $confirmation où l'on affiche un message
	# grâce à la variable $_GET['mailok']
	@header('Location: '.$confirmation.'?mailok=1');
	exit;
	
}
?>
<!-- formulaire simplifié -->
<!-- script développé par Pierre Pesty http://dev.ppan.net -->
<form action="<?= $_SERVER['PHP_SELF']?>" method="post">
<? if(strlen($erreur)) { ?>
<? echo "$erreur\n"?>
<? } ?>
<input name="Nom" value="<?= htmlentities($_POST['Nom'],ENT_QUOTES)?>">
<? if($imagecode) { ?>
<? echo "$imagecode\n"?><input type="text" name="vateuf" size="<?= $nbrchars?>" maxlength="<?= $nbrchars?>" value="<?= htmlentities($_POST['vateuf'],ENT_QUOTES)?>">
 Recopiez les caractères à gauche SVP <a href="<?= $_SERVER['PHP_SELF']."?newcode=1"?>">Nouveau</a>
<? } ?>
<input type="submit" value="Envoyer">
</form>

Variante

Pour les paranos, on peut remplacer les caractères uniques (imaginant que les robots vont lire le code dans les balises <img>) par des fichiers images contenant une série de codes. Auquel cas on devra implémenter un tableau à double entrée : nom de l'image => code correspondant. Le nom de l'image n'ayant alors plus aucune importance. Ou encore écrire l'image via la librairie GD, sur le modèle écrit par l'ami Erwan :

[Haut de page]

<?
// par Erwan http://www.kafarnaum.net/
if ($string == "")
$string = "nothing";
$l = strlen($string);
$WIDTH = $l * $xchar * 2;
$HEIGHT = $ychar * 3;

$img = imagecreatetruecolor($WIDTH, $HEIGHT);
imageantialias($img, true); // selon config PHP
$WIDTH--;
$HEIGHT--;
$clear = imagecolorallocatealpha($img, 255, 255, 255, 0);
$black = imagecolorallocatealpha($img, 0, 0, 0, 0);
$grey = imagecolorallocatealpha($img, 127, 127, 127, 0);

imagefilledrectangle($img, 0, 0, $WIDTH, $HEIGHT, $clear);
for ($i = 0; $i < $l; $i++) {
  $x1 = rand($xchar * 2 * $i, $xchar * 2 * ($i + 1));
  $x2 = rand($xchar * 2 * $i, $xchar * 2 * ($i + 1));
  imageline($img, $x1, 0, $x2, $HEIGHT, $grey);
}
imageline($img, 0, rand(0, $HEIGHT), $WIDTH, rand(0, $HEIGHT), $grey);
for ($i = 0; $i < $l; $i++) {
  $x = rand($xchar * 2 * $i, $xchar * 2 * ($i + 1) - $xchar);
  $y = rand(0, $HEIGHT - $ychar * 1.5);
  imagestring($img, $font, $x, $y, $string[$i], $black);
}

header("Content-type: image/png");
imagepng($img);
imagedestroy($img);
?>

Contrôle des en-têtes

Sur le modèle décrit en détail sur cette page www.phpsecure.info on peut se passer du subterfuge code/image, en contrôlant les en-têtes ainsi que le corps du message. Partant du principe qu'aucun saut de ligne (\n ou \r) n'est accepté dans $headers ou $destinataire que des entrées de type bcc: ou cc: ou from: n'ont rien à faire dans le corps du message $message_final, on peut insérer :

[Haut de page]

<?
$erreur = "";
$headers = "From: ".$POST['Email'];
// avant l'envoi du mail test des champs sensibles
if(preg_match("/\r|\n/",$headers) || preg_match("/\r|\n/",$destinataire) || preg_match("/cc:|bcc:|from:/i",$message_final))
	$erreur = "Tu t'es vu quand tu spam ?..."; // bloquage du mail
// ajout des en-têtes optionnelles après vérification
if(strlen($email_cc))
	$headers .= "\nCC: ".$email_cc;
if(strlen($email_bcc))
	$headers .= "\nBCC: ".$email_bcc;
// pas d'erreur...
if($_POST && !strlen($erreur)) {
	// ... traitement du formulaire
} elseif(strlen($erreur)) {
	echo $erreur;
}
?>

http://dev.ppan.net [Haut de page]