I form sono una delle parti più importanti per quanto riguarda il web in generale. Immagazzinare informazioni attraverso l'inserimento di queste da parte degli utenti che navigano il nostro sito è pratica comune, indipendentemente dalla semplicità o dalla complessità delle operazioni che si stanno effettuando. Purtroppo lo spam ha portato con sè una serie di problemi legati all'immissione automatica dei dati all'interno dei form presenti su un sito internet: esistono dei bot (script creati appositamente per emulare il comportamento di una persona fisica nell'esecuzione di determinati compiti) che scansionano la rete alla ricerca di form ai quali inviano grosse quantità di messaggi indesiderati.
Fortunatamente sono nate delle soluzioni che possono essere utilizzate per frenare (o dove non è possibile, intralciare pesantemente) questi bot in modo che non ci inviino dati indesiderati. Una di queste soluzioni, che è anche la più utilizzata, è chiamata CAPTCHA (dall'acronimo Completely Automated Public Turing test to tell Computers and Humans Apart) e prevede l'aggiunta al form di un'immagine generata casualmente contenente del testo con caratteri distorti. Il contenuto di questa immagine deve essere immesso in un apposito campo e viene validato dal server: in caso la stringa non risulti corretta il form non viene inviato e, solitamente, viene richiesta nuovamente l'immissione dei dati.
In questo breve articolo vediamo come scrivere un semplice script che si occupi di farci ottenere l'obiettivo di evitare che dei bot inviino dati indesiderati al nostro server. Ovviamente l'obiettivo che ci poniamo è puramente didattico, e per un ambiente di produzione consiglio vivamente di studiare soluzioni appropriate partendo da quanto proposto in questa sede. Cominciamo!
Generazione di un'immagine casuale
Il primo problema da risolvere per quanto riguarda l'implementazione del sistema è trovare un modo per generare immagini casuali che risultino allo stesso tempo comprensibili dall'utente ma incomprensibili ai bot che analizzano i nostri form. La generazione casuale dell'immagine solitamente si basa sull'utilizzo di un'immagine di background con colori eterogenei, sulla quale viene disegnata la stringa di validazione utilizzando come colore uno di quelli recuperati dall'immagine schiarito o scurito. In aggiunta spesso l'immagine viene inquinata con linee e punti casuali per rendere complessa l'edge detection (filtro che, insieme ad altri, viene spesso utilizzato per cercare di estrarre la stringa dall'immagine in modo automatico) ed il font scelto viene ruotato.
è ovvio che in una situazione simile l'immagine deve essere abbastanza complessa da poter difficilmente esser riconosciuta da un programma automatico (che, ricordo, prevede sempre un margine di errore nel riconoscimento, quindi non bastano piccole distorsioni dell'immagine per essere sicuri che non vengano riconosciute automaticamente) ma abbastanza semplice da comprendere per un essere umano.
Lo script che andremo a creare non utilizzerà cookie o sessioni per salvare le informazioni per la validazione dei dati, ma si baserà su una cooperazione tra lo script che genera l'immagine e quello che genera il form: in pratica la pagina che visualizza il form genererà una stringa casuale che verrà salvata in un database al quale verrà associata una chiave casuale univoca. Questa chiave verrà passata come parametro allo script per la generazione dell'immagine che recupererà dal database la stringa e la renderizzerà. La chiave verrà passata in un campo hidden alla pagina di destinazione che si occuperà di effettuare la validazione. La chiave potrà essere utilizzata una sola volta, indipendentemente dal fatto che sia stata immesse correttamente o meno.
La prima operazione quindi è creare la tabella che utilizzeremo per la comunicazione:
CREATE TABLE validation ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, url_key CHAR(40) UNIQUE NOT NULL, captcha CHAR(32) NOT NULL, expire_date DATETIME NOT NULL, PRIMARY KEY(id), INDEX(url_key) );
Il campo url_key
conterrà la chiave (generata casualmente e criptata utilizzando l'algoritmo SHA1), il campo captcha la stringa di controllo ed expire_date
sarà la data dopo la quale la chiave verrà automaticamente indicata come scaduta e quindi non potrà più essere utilizzata.
Successivamente possiamo procedere con la stesura dello script per la generazione dell'immagine. Il processo può essere più o meno complesso, quindi mi limiterò a fornirne uno molto semplice che utilizzerà una texture di sfondo sulla quale verrà generata una stringa utilizzando uno dei font forniti con le librerie GD. Penso sia ovvio che lo script in questione non è sicuro e viene fornito solo come esempio per permettere il corretto funzionamento del programma.
<?php $pdo = new PDO('mysql:host=localhost;dbname=captcha', 'root', ''); $stmt = $pdo->prepare("SELECT * FROM validation WHERE url_key = ? AND expire_date > NOW()"); $stmt->execute(array($_GET['token'])); $row = $stmt->fetch(PDO::FETCH_ASSOC); $texture = imagecreatefrompng('texture.png'); $source = imagecolorat($texture, rand(0, imagesx($texture)), rand(0, imagesy($texture))); $r = ($source >> 16 & 0xff) + 50; $g = ($source >> 8 & 0xff) + 50; $b = ($source & 0xff) + 50; $text_color = imagecolorallocate($texture, $r, $g, $b); imagestring($texture, 5, (imagesx($texture) - strlen($row['captcha']) * 5)/ 2, 5, $row['captcha'], $text_color); header('Content-Type: image/png'); imagepng($texture); imagedestroy($texture); ?>
Lasciando stare la fase di generazione dell'immagine, è importante notare che la stringa da visualizzare viene recuperata dal database, facendo attenzione che la chiave specificata via GET non sia già scaduta.
Controllo della corretta immissione
Ora procediamo con lo script che si occuperà della generazione del form e della sua validazione:
<?php function random_string($len) { $string = ""; $chars = array("a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"); for($i = 0; $i < $len; ++$i) { shuffle($chars); $string .= $chars[0]; } return $string; } $message = 'Inserisci la scringa alphanumerica per effettuare la validazione:'; $pdo = new PDO('mysql:host=localhost;dbname=captcha', 'root', ''); $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); if(isset($_POST['validate_token'])) { // Eseguo la validazione $stmt = $pdo->prepare("SELECT * FROM validation WHERE url_key = ? AND expire_date > NOW()"); $stmt->execute(array($_POST['token'])); $row = $stmt->fetch(PDO::FETCH_ASSOC); $message = ($row && ($row['captcha'] == $_POST['validate_token'])) ? 'token corretto' : 'token <strong>NON</strong> corretto'; $stmt = $pdo->prepare("DELETE FROM validation WHERE url_key = ?"); $stmt->execute(array($_POST['token'])); } // Elimino tutti i record scaduti $pdo->query("DELETE FROM validation WHERE expire_date <= NOW()"); // Genero casualmente un record per la tabella validation $url_key = sha1(uniqid(rand(), true)); // Possiamo (e dovremmo) fare meglio $captcha = random_string(6); $stmt = $pdo->prepare("INSERT INTO validation (id, url_key, captcha, expire_date) VALUES ('', ?, ?, DATE_ADD(NOW(), INTERVAL 5 MINUTE))"); $stmt->execute(array($url_key, $captcha)); ?> <html> <head> <title>CAPTCHA test</title> </head> <body> <form method="POST" action="<?php echo $_SERVER['PHP_SELF']; ?>"> <input type="hidden" name="token" value="<?php echo $url_key; ?>" /> <p> <?php echo $message; ?> </p> <img src="captcha.php?token=<?php echo $url_key; ?>" width="200" height="30" /><br/> <input type="text" name="validate_token" /> <input type="submit" name="action" value="Test" /> </form> </body> </html>
Ho cercato di mantenere lo script in più semplice possibile, utilizzando codice inline ed la programmazione procedurale che non sono solitamente il metodo che preferisco per programmare; analizzano lo script possiamo evidenziare vari aspetti:
- La funzione
random_string
utilizzata per la generazione casuale della stringa da utilizzare - Per far sì che una stringa sia utilizzabile una sola volta, il record ad essa corrispondente viene rimosso sia nel caso che la validazione vada a buon fine che in caso contrario
- Ogni volta che la pagina viene visualizzata senza che vi siano inviati dei dati in POST, ci occupiamo di svuotare la tabella eliminando i record scaduti e successivamente generiamo la chiave e la stringa di controllo. Entrambe sono generate utilizzando sistemi casuali molto semplici, che dovrebbero essere migliorati in modo da aumentare l'affidabilità in caso fosse necessario
- Al submit del form viene effettuato il vero e proprio controllo dei dati immessi utilizzando una semplice query SQL
Seguendo questi semplici step abbiamo praticamente terminato lo script ottenendo il risultato richiesto con il minimo sforzo.
Conclusione
Siamo giunti alla fine dell'articolo. Spero possa essere stato utile dato che, diversamente da come fanno la maggior parte dei tutorial che si trovano in rete, ho cercato di evitare l'utilizzo di sessioni e cookie in modo da rendere lo script il più compatibile possibile con le impostazioni dei browser. Prima di chiudere voglio ricordare che è necessario fare molta attenzione nell'utilizzo di questi sistemi. I form generati utilizzando questa tecnica non sono difatti accessibili per le persone con disturbi più o meno gravi alla vista; per ovviare a questo problema è buona norma affiancare a questo CAPTCHA anche uno sonoro, che legga la chiave nella lingua di chi deve immettere i dati. Questo argomento è leggermente più complesso e verrà analizzato in un altro articolo.