Proteggere un form dallo spam con PHP

In questo articolo proveremo ad illustrarvi come proteggere un form HTML dall’invio di SPAM mediante la tecnica del CAPTCHA. I form html non gestiti in modo ottimale possono essere causa di problemi sia nel caso in cui il modulo invii delle informazioni tramite email che nel caso inserisca le informazioni in un database.

Il rischio principale è costituito dagli SPAMBOT ovvero dei software in grado di effettuare richieste http allo scopo di bucare i form non protetti. Le conseguenze di un attacco di questo tipo possono essere il sovraccarico del server web, malfunzionamenti del server di posta nonchè il rapido accumularsi di informazioni inutili sul Database.Il potenziale pericolo è rappresentato dal fatto che analizzando i tag di una pagina html, si possono analizzare facilmente le variabili usate per il passaggio delle informazioni, il metodo utilizzato (POST/GET) e la pagina che elabora la richiesta di invio email.

Quindi un qualsiasi individuo che vuole crearvi una grana, può, mediante un banale programmino che invia richieste HTTP effettuare la medesima richiesta infinite volte e intasarvi la posta o riempirvi il database. Per questo problema nasce l’esigenza di validare lato server le richieste effettuate tramite la pagina che si occupa nello specifico di elaborare la richiesta.

La tecnica del Captcha Code “completely automated public Turing test to tell computers and humans apart” è uno dei metodi più utilizzati per verificare se chi effettua la richiesta è un umano oppure un BOT.

A questo punto mostriamo un esempio di come implementare tale tecnica all’interno del proprio sito web:

Iniziamo con la creazione di una tabella sul nostro DB MySql che contenga le informazioni necessarie per implementare la tecnica del Captcha. Connettiamoci al nostro DB tramite PhpMyAdmin o tramite linea di comando e creiamo la tabella captcha con il seguente script:

CREATE TABLE `captcha` (
`captcha_id` varchar(255) character set latin1 collate latin1_general_ci NOT NULL,
`code` varchar(255) character set latin1 collate latin1_general_ci NOT NULL,
`timestamp` varchar(255) character set latin1 collate latin1_general_ci NOT NULL default ''
) ENGINE=MyISAM DEFAULT CHARSET=latin1;

Occupiamoci ora della pagina dove verrà visualizzato il FORM, preleviamo il codice e salviamolo in un file di testo index.php (per comodità si consiglia di posizionare tutti i file creati all’interno della stessa directory nella root del web server) :

<?php
echo "<html>";
echo "<head>";

// genero un identificativo criptato md5 da associare al codice di sicurezza
$captcha_id = md5(microtime().mt_rand().'super1segreto2segretissimo3');

// creo un codice di controllo per la verifica dell'identificativo
$sale = "frase super segreta da cambiare a discrezione.";

// cripto in md5 i due codici generati
$chk = md5($sale.$captcha_id);

echo "</head>";
echo "<body>";

//Creo il form

echo "<form method=\"post\" action=\"email.php\">";
echo "Cognome : <input type=\"text\" name=\"surname\"/>";
echo "<br/>";
echo "Nome : <input type=\"text\" name=\"name\"/>";
echo "<br/>";
echo "E-mail : <input type=\"text\" name=\"mail\"/>";
echo "<br/>";
echo "<img src=\"captcha.php?id=".$captcha_id."&chk=".$chk."\" width=\"110\" height=\"70\"/>";
echo "<input type=\"text\" name=\"captcha_code\"/>";
echo "<br/>";
echo "<input name=\"captcha_id\" type=\"hidden\" id=\"captcha_id\" value=\"".$captcha_id."\"/>";
echo "<br/>";
echo "<input type=\"submit\" name=\"submit\" value=\"Submit\"/>";
echo "<br/>";
echo "</form>";

echo "</body>";
?>

Notare bene che nello script precedente:

l’immagine Captcha è stata generata dalla richiesta presente nell’attributo src dell’immagine
il campo captcha_code conterrà il codice di sicurezza che l’utente dovrà inserire per validare l’elaborazione
il campo captcha_id contiene il captcha_id generato precedentemente nella head
Ecco il listato della pagina che genera l’immagine CAPTCHA, salviamo il codice in un file di testo avendo l’accortezza di salvarlo come captcha.php :

<?php
// controllo se è stato fornito l identificativo del codice captcha
if(!isset($_GET['id'])) exit();

// controllo la validità dell identificativo
$captcha_id = $_GET['id'];
if(!preg_match("/^[a-f0-9]{32}$/",$captcha_id)) exit();

// ricreo il codice di controllo e lo confronto con quello passato via GET
$sale = "frase super segreta da cambiare a discrezione.";
$chk = md5($sale . $captcha_id);
if($chk != $_GET['chk']) exit();

// creo l immagine
// ---------------------------------------------------------------

// le dimensioni dell immagine
$size_x = 110;
$size_y = 70;

$img = imagecreatetruecolor($size_x,$size_y);

// alloco un colore per lo sfondo
$backgroung = imagecolorallocate($img,255, 255, 255);

// alloco 3 colori per le 3 lettere da decifrare
// utilizzo il canale alpha per impostare la trasparenza
$color[] = imagecolorallocatealpha($img,110,110,110,60);
$color[] = imagecolorallocatealpha($img,3,187,63,60);
$color[] = imagecolorallocatealpha($img,255,0,0,70);

// mischio l ordine dei colori
shuffle($color);

// coloro lo sfondo creando un rettangolo
imagefilledrectangle($img,0,0,$size_x-1,$size_y-1,$backgroung);

// i caratteri da utilizzare per il codice di sicurezza
$caratteri = "ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789";

// inizializzo la variabile che conterrà il codice
$codice = '';

// per ciascuno dei 3 caratteri
for ($i=0 ; $i < 3 ; $i++){
// estraggo un carattere a caso
$codice .= $caratteri{rand(0,34)};
// disegno il carattere
imagettftext(
$img,
50,
-5+rand(0,10), // rotazione casuale
($i+0.3)*24,
60,
$color[$i],
'./VeraMono.ttf',
$codice{$i});
}

// inserisco identificativo e codice nel DB
// ---------------------------------------------------------------
mysql_connect('IP_MIO_SERVER_MYSQL','USER_MYSQL','PASS_MYSQL');
mysql_select_db('NOME_DB_MYSQL');

mysql_query("REPLACE INTO captcha (captcha_id,code,`timestamp`)
values ('$captcha_id','$codice',UNIX_TIMESTAMP())");

// con prob. impostata da $probabilità cancello i record più vecchi
$probabilità = 5; // 5%
if(mt_rand(1,100) <= $probabilità){
$durata_captcha = 60*60;
mysql_query("DELETE FROM captcha WHERE `timestamp`<(UNIX_TIMESTAMP()-$durata_captcha)");
}

// invio l immagine al browser
// ---------------------------------------------------------------
header("Content-type: image/png");
imagepng($img);
?>

Da notare che nello script precedente si utilizza un font per generare l’immagine (VeraMono.ttf)  il quale può essere modificato a proprio piacimento con un qualsiasi font installato nel proprio pc o scaricato da internet. Per scaricare il font utilizzato nell’esempio cliccare qui. Nell’esempio il font in questione è posizionato all’interno della stessa cartella dove sono stati posizionati gli script.

Passiamo ora ad esaminare la pagina che invierà l’ e-mail e che si occuperà anche di verificare la validità dell’elaborazione, copiamo il codice sottostante e incolliamolo su un file di testo avendo l’accortezza di salvarlo come email.php.

<?php
//Inclusione file esterni
 require_once('send_mail.php');

// Recupero dei dati POST
$surname = $_POST['surname'];
$name = $_POST['name'];
$mail = $_POST['mail'];

// dopo quanto tempo i dati nella tabella del DB scadono
$durata_captcha = 60*60;

// ci sono tutti i dati?
if(!isset($_POST['captcha_code']) OR !isset($_POST['captcha_id'])) exit;
$captcha_code = $_POST['captcha_code'];
$captcha_id = $_POST['captcha_id'];

// controlla la validità del $captcha_id
if(!preg_match("/^[a-f0-9]{32}$/",$captcha_id)){
// se il $captcha_id non è valido termino la procedura
echo '$captcha_id non valido';
}else{
// controlla la validità del $captcha_code
if (!preg_match("/^[a-zA-Z0-9]{3}$/", $captcha_code)){
// se il $captcha_code non è valido non faccio la ricerca nel DB
echo '$captcha_code non valido';
}else{
// $captcha_id e $captcha_code sono ben formattati, posso fare la verifica
// rendo tutte le lettere del $captcha_code maiuscole e
// sostituisco gli zeri con le O
$captcha_code = strtoupper($captcha_code);
$captcha_code = str_replace('0','O',$captcha_code);

mysql_connect('IP_MIO_SERVER_MYSQL','USER_MYSQL','PASS_MYSQL') or die(mysql_error());
mysql_select_db('NOME_DB_MYSQL') or die(mysql_error());

// nella verifica non considero dati più vecchi di $durata_captcha secondi
$res = mysql_query("SELECT code FROM captcha WHERE captcha_id = '$captcha_id' AND code = '$captcha_code' AND `timestamp` > (UNIX_TIMESTAMP() - $durata_captcha)") or die (mysql_error());
if(mysql_num_rows($res) != 1){
// verifica fallita
echo '$captcha_code errato';
}else{
// cancello i vecchi codici e quello corrente
mysql_query("DELETE FROM captcha WHERE captcha_id = '$captcha_id' OR `timestamp` < (UNIX_TIMESTAMP() - $durata_captcha)");

//Scrittura parametri email
$from_name  = $name." ".$surname;
$from_email = $mail;
$to_name    = "NOME_DEL_DESTINATARIO";
$to_email   = "MAIL_DEL_DESTINATARIO";
$subject    = "Invio email sicura dal tuo sito";
$text_message = "This is HTML email and your email client software ain't support HTML email.";
$html_message = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\">\n";
$html_message.= "<html><head><title></title>\n";
$html_message.= "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=iso-8859-1\">\n";
$html_message.= "</head>\n";
$html_message.= "<body>";
$html_message.= "Ciao, questa è un email sicura inviata dal tuo sito";
$html_message.= "</body></html>";

//Creo l'allegato
$attachment = array ("PATH_ALLEGATO_SUL_SERVER");

//Invio l'email con la funzione sendmail
sendmail ($from_name, $from_email, $to_name, $to_email, $subject, $text_message, $html_message, $attachment);

echo "<script language=\"Javascript\">";
echo "<!–-";
echo "alert (\"e-mail inviata con successo\");";
echo "history.go(-1);";
echo "–->";
echo "</script>";
}
}
}
?>

I controlli effettuati all’inizio del codice illustrato permettono al server web di distinguere se si tratta di una richiesta valida o di una richiesta effettuata da un BOT.

Se i controlli saranno superati si passerà ad invocare la funzione sendmail che si occuperà dell’invio di una mail in formato HTML con allegati. Alternativamente a questa funzione si pùo invocare la classica funzione mail disponibile nel linguaggio PHP.

Ecco ora il listato della funzione sendmail invocata nel precedente script, che si occupa di inviare una mail in formato HTML. Copiare il codice e salvarlo in un file denominato send_mail.php.

<?php
function sendmail ($from_name, $from_email, $to_name, $to_email, $subject, $text_message="", $html_message, $attachment=""){
$from = "$from_name <$from_email>";
$to   = "$to_name <$to_email>";
$main_boundary = "----=_NextPart_".md5(rand());
$text_boundary = "----=_NextPart_".md5(rand());
$html_boundary = "----=_NextPart_".md5(rand());
$headers  = "From: $from\n";
$headers .= "Reply-To: $from\n";
$headers .= "X-Mailer: Hermawan Haryanto (http://hermawan.com)\n";
$headers .= "MIME-Version: 1.0\n";
$headers .= "Content-Type: multipart/mixed;\n\tboundary=\"$main_boundary\"\n";
$message .= "\n--$main_boundary\n";
$message .= "Content-Type: multipart/alternative;\n\tboundary=\"$text_boundary\"\n";
$message .= "\n--$text_boundary\n";
$message .= "Content-Type: text/plain; charset=\"ISO-8859-1\"\n";
$message .= "Content-Transfer-Encoding: 7bit\n\n";
$message .= ($text_message!="")?"$text_message":"Text portion of HTML Email";
$message .= "\n--$text_boundary\n";
$message .= "Content-Type: multipart/related;\n\tboundary=\"$html_boundary\"\n";
$message .= "\n--$html_boundary\n";
$message .= "Content-Type: text/html; charset=\"ISO-8859-1\"\n";
$message .= "Content-Transfer-Encoding: quoted-printable\n\n";
$message .= str_replace ("=", "=3D", $html_message)."\n";
if (isset ($attachment) && $attachment != "" && count ($attachment) >= 1){
for ($i=0; $i<count ($attachment); $i++){
$attfile = $attachment[$i];
$file_name = basename ($attfile);
$fp = fopen ($attfile, "r");
$fcontent = "";
while (!feof ($fp)){
$fcontent .= fgets ($fp, 1024);
}
$fcontent = chunk_split (base64_encode($fcontent));
@fclose ($fp);
$message .= "\n--$html_boundary\n";
$message .= "Content-Type: application/octetstream\n";
$message .= "Content-Transfer-Encoding: base64\n";
$message .= "Content-Disposition: inline; filename=\"$file_name\"\n";
$message .= "Content-ID: <$file_name>\n\n";
$message .= $fcontent;
}
}
$message .= "\n--$html_boundary--\n";
$message .= "\n--$text_boundary--\n";
$message .= "\n--$main_boundary--\n";
@mail ($to, $subject, $message, $headers);
}
?>

A questo punto possiamo testare lo script chiamando la pagina index.php dal nostro browser.

Alla prossima…

Tags: ,