PHP Capatcha Script

by

So ziemlich jeder der ein Kontaktformular, Gästebuch oder auch Forum auf seiner Webseite betreibt hat schon einmal Bekanntschaft mit dieser verhassten Gattung gemacht: Spam-Bots!

Die ultimative Lösung gegen Spam-Bots gibt es nicht, aber man kann ihnen dennoch mit relativ einfachen Mitteln das Leben wesentlich erschweren. Eine sehr einfache und zugleich effektive Maßnahme ist das einbauen eines sogenannten Captcha.

Der Ausdruck Captcha steht für Completely Automated Public Turing test to tell Computers and Humans Apart und ist vereinfacht ausgedrückt eine Methode, die menschliche Interaktion bedarf um eine bestimmte Aktion auf einer Webseite auszuführen. Eine ausführliche Erklärung dazu findet man in diesem Artikel bei Wikipedia

In diesem HowTo erstelle ich ein einfaches Captcha-Script und anhand eines Pseudo-Kontaktformulars wird demonstriert wie man auch nachträglich noch ein Captcha in sein Formular, Gästebuch oder was auch immer, einbauen kann.

Pseudo-Kontaktformular sage ich deswegen, weil die Mail-Funktion nicht eingebaut ist und eine anständige Überprüfung der Benutzereingaben nicht statt findet.

Voraussetzungen

Um das hier gezeigte erfolgreich in eigene Projekte zu übernehmen benötigt man

  • PHP Kenntnisse
  • Webserver mit GD2 Unterstützung
  • Lernbereitschaft & Zeit

Ob der Webserver, auf dem das Script laufen soll, GD2 Unterstützung bietet kann man sehr einfach herausfinden indem man phpinfo() ausführt und den Abschnitt GD unter die Lupe nimmt.

Kontaktformular ohne Captcha

Nachfolgend der Quelltext unseres Beispiel-Kontaktformulars das wir nachträglich mit einem Captcha erweitern wollen. Um Platz zu sparen ist das Script etwas gekürzt und der CSS Teil wurde entfernt. In der angehängen Datei ist jedoch das komplette Script enthalten.

<?php

// Variablen initialisieren

$NameFehler = $EmailFehler = $NachrichtFehler = FALSE;

$Fehlerfrei = TRUE;

// Formular wurde abgeschickt

if (isset( $_POST['senden'] ))

{

// Felder auf Inhalt pruefen

if (strlen( trim( $_POST['name'] ) ) < 3)

{

$Fehlerfrei = FALSE;

$NameFehler = TRUE;

}

if (strlen( trim( $_POST['email'] ) ) < 3)

{

$Fehlerfrei = FALSE;

$EmailFehler = TRUE;

}

if (strlen( trim( $_POST['nachricht'] ) ) < 3)

{

$Fehlerfrei = FALSE;

$NachrichtFehler = TRUE;

}

// Wenn alle Felder ausgefuellt wurden und der Captcha-Code korrekt war

if ($Fehlerfrei)

{

// Code zum Email-Versand ausfuehren

echo "<h2>Das Formular wurde korrekt ausgefuellt!</h2>";

}

}

?>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="de" lang="de">

<head>

<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />

<title>Kontaktformular mit Captcha</title>

</head>

<body>

<form id="kontaktformular" action="<?php echo $_SERVER['PHP_SELF']; ?>" method="post">

<fieldset>

<legend>Kontaktformular</legend>

<label for="name"><?php echo $NameFehler ? '<span style="color: #FF0000;">Ihr Name:</span>' : 'Ihr Name:'; ?></label>

<input name="name" id="name" type="text" value="<?php echo $_POST['name']; ?>" />

<label for="email"><?php echo $EmailFehler ? '<span style="color: #FF0000;">Ihre Email:</span>' : 'Ihre Email:'; ?></label>

<input name="email" id="email" type="text" value="<?php echo $_POST['email']; ?>" />

<label for="nachricht"><?php echo $NachrichtFehler ? '<span style="color: #FF0000;">Ihre Nachricht:</span>' : 'Ihre Nachricht:'; ?></label>

<textarea rows="6" name="nachricht" id="nachricht"><?php echo $_POST['nachricht']; ?></textarea>

<input name="senden" id="senden" type="submit" value="Absenden" class="button" />

</fieldset>

</form>

</body>

</html>

Wie man unschwer sieht ist dieses Script alles andere als sicher. Für ein Spam-Bot wäre es ein leichtes tausende von Emails an unzählige User zu senden. Ja korrekt gelesen; Spam-Bots haben es nicht darauf abgesehen einem einzelnen Benutzer das Postfach zu verstopfen, sondern sind viel eher darauf aus ihre unerwünschte Botschaften und Werbung an tausende Benutzer gleichzeitig zu verteilen. Man kann sich wohl zu 99% sicher sein das die tägliche Pharma-Werbung in der Mailbox über unsichere und nachlässig programmierte Scripts von Hobbywebmaster kommen.

Aber zurück zu unserem Kontaktformular. Damit wir einen Sicherheitscode einbauen können benötigen wir natürlich erst mal ein Script das uns selbigen erzeugt.

Captcha Script

Zunächst gibt es einmal das komplette Listing des Scripts und im Anschluß daran schauen wir es uns Zeile für Zeile an und durchleuchten die arbeitsweise. Wie dabei schnell ersichtlich wird muß man kein PHP Profi sein um dynamische Grafiken zu erzeugen.

Eine wichtige Anmerkung möchte ich noch machen bevor es nun los geht. Es gibt verschiedene Techniken und Vorlieben wie man ein Captcha erzeugen kann. Die hier gezeigte Variante ist eher schlicht gehalten, ohne großen Schnickschnack oder komplizierte Zufalls-Generatoren. Wir wollen aber auch kein Fort Knox erschaffen sondern lediglich ein einfaches Captcha erzeugen um den Spam-Bots das Leben schwerer zu machen.

<?php

// Session starten

session_start();

// Alten Captcha-Code aus der Session loeschen

unset( $_SESSION['captcha_code'] );

// Das Cachen der Grafik verhindern

header( "Expires: Mon, 26 Jul 1997 05:00:00 GMT" );

header( "Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT" );

header( "Cache-Control: no-store, no-cache, must-revalidate" );

header( "Cache-Control: post-check=0, pre-check=0", false );

header( "Pragma: no-cache" );

// Dem Browser mitteilen das es sich hierbei um ein JPG handelt.

header( 'Content-type: image/jpeg' );

// Sicherheitscode generieren

$AlphaNumerischerString = "ABCDEFGH2345689";

$ZufallString1 = substr( str_shuffle( $AlphaNumerischerString ), 0, 2 );

$ZufallString2 = substr( str_shuffle( $AlphaNumerischerString ), 0, 2 );

$ZufallString3 = substr( str_shuffle( $AlphaNumerischerString ), 0, 2 );

$ZufallStringKomplett = $ZufallString1.$ZufallString2.$ZufallString3;

// Sicherheitscode in der Session speichern

$_SESSION['captcha_code'] = md5( $ZufallStringKomplett );

// Grafik erzeugen und an den Browser senden

$Schriftarten = array( "zachary.ttf", "mtcorsva.ttf", "gilligan.ttf");

$Bilddatei = imagecreatefrompng( "hintergrund.png" );

$TextFarbe1 = imagecolorallocate( $Bilddatei, 0, 125, 0 );

$TextFarbe2 = imagecolorallocate( $Bilddatei, 130, 70, 90 );

$TextFarbe3 = imagecolorallocate( $Bilddatei, 180, 90, 190 );

imagettftext( $Bilddatei, 12, 15, 3, 24, $TextFarbe1, $Schriftarten[0], $ZufallString1 );

imagettftext( $Bilddatei, 16, 0, 26, 15, $TextFarbe2, $Schriftarten[1], $ZufallString2 );

imagettftext( $Bilddatei, 14, -20, 53, 18, $TextFarbe3, $Schriftarten[2], $ZufallString3 );

imagejpeg( $Bilddatei );

// Grafik zerstören und Speicher freigeben

imagedestroy( $Bilddatei );

?>

Ich habe versucht aussagekraeftige Variablennamen zu benutzen, damit man leichter nachvollziehen kann wann was wo passiert. Sehr viele Tutorials benutzen oft unsinnige, nichts sagende Variablennamen (Gruß an die abgedroschenen $x, $foo, $bar und Konsorten) und erschweren einem Anfänger das verstehen eines Script – zumindest ging es mir immer so.

Machen wir uns nun also an das zerlegen des Scripts

session_start();

Das dürfte bekannt sein. Hier wird eine Session gestartet in die wir weiter unten den Sicherheitscode ablegen, damit wir später einen Wert zum vergleichen haben.

unset( $_SESSION['captcha_code'] );

Vorsichtshalber wird ein eventuell existierender Code gelöscht. Es kann deswegen bereits ein Code in der Session stehen, weil zuvor das Formular fehlerhaft abgeschickt wurde. In dem Fall wird automatisch ein neuer Code erzeugt und der alte Wert ist unnötig.

header( "Expires: Mon, 26 Jul 1997 05:00:00 GMT" );

header( "Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT" );

header( "Cache-Control: no-store, no-cache, must-revalidate" );

header( "Cache-Control: post-check=0, pre-check=0", false );

header( "Pragma: no-cache" );

Diese Header-Anweisungen verhindern das die erzeugte Grafik im Cache landet und somit eventuell einen falsche Sicherheitscode im Browser anzeigt. Diese Anweisung ist quasi Standard und wird immer dann an einen Browser gesendet, wenn man das cachen verhindern möchte – nicht nur bei einem Captcha.

header( 'Content-type: image/jpeg' );

Eine wichtige Zeile Code. Durch sie wird dem Browser mitgeteilt das dieses Script als Resultat ein JPG sendet.

$AlphaNumerischerString = "ABCDEFGH2345689";

Das sind die Buchstaben und Zahlen aus denen der Sicherheitscode erzeugt wird. Mag dem einen oder anderen sehr kurz erscheinen aber es ist ausreichend. Wie man sieht habe ich darauf verzichtet Buchstaben und Zahlen zu benutzen bei denen Verwechslungsgefahr besteht. Eine 1 könnte sonst mit l (kleines L) oder I (grosses i) verwechselt werden. Gleiches gilt für O (grosses o) und 0 (Null), usw.

$ZufallString1 = substr( str_shuffle( $AlphaNumerischerString ), 0, 2 );

$ZufallString2 = substr( str_shuffle( $AlphaNumerischerString ), 0, 2 );

$ZufallString3 = substr( str_shuffle( $AlphaNumerischerString ), 0, 2 );

Hier werden 3 Zeichenketten erzeugt die jeweils 2 Zeichen lang sind. Dazu wird mit str_shuffle( $AlphaNumerischerString ) die oben stehende Zeichenketten gemischt (shuffle) und mit substr( „…“, 0, 2 ) jeweils die ersten beiden Zeichen in die entsprechende Variable abgelegt.

$ZufallStringKomplett = $ZufallString1.$ZufallString2.$ZufallString3;

Setzt die 3 eben erzeugten Einzelteile zu einem einzigen String zusammen der mit…

$_SESSION['captcha_code'] = md5( $ZufallStringKomplett );

…in der Session abgelegt wird. Genauer gesagt wird mit md5() von dem String ein Hash-Wert erzeugt der in der Session gespeichert wird. Das wird der Wert sein den wir später im Kontaktformular überprüfen werden.

Hier wird auch klar wieso wir dazu eine Session benötigen, denn ohne sie könnten wir den Sicherheitscode nicht zwischen den beiden Scripts transportieren.

Jetzt folgt der Teil des Script das für die dynamischen Grafikerzeugung verantwortlich ist.

$Schriftarten = array( "zachary.ttf", "mtcorsva.ttf", "gilligan.ttf");

Zunächst wird ein Array erstellt das die Namen der verwendeten Schriftarten enthält. Hier hat man mehr oder weniger freie Hand welche Schriften man benutzen möchte. Es ist aber zu beachten das die Schriften mit auf den FTP übertragen werden müssen und dort Speicher (Festplatte und Arbeitsspeicher) belegen.

Der Grund wieso hier verschiedene Schriftarten benutzt werden ist, daß es Bots gibt die Standardschriftarten erkennen und so den Sicherheitscode aus einer Grafik extrahieren können. Ich habe einmal einen Artikel gelesen der diesen Prozess beschrieb den Bots anwenden um einfache CaptchaS auszutricksen. Leider habe ich den Bookmark nicht mehr und weiß auch nicht mehr auf welcher Seite ich diesen Artikel gelesen habe.

$Bilddatei = imagecreatefrompng( "hintergrund.png" );

Es wird eine Grundgrafik erzeugt auf die der Sicherheitscode projiziert wird. In unserem Fall benutzen wir eine vorbereitete Grafik mit farblichem Muster. Das erschwert es einem Bot enorm den Sicherheitscode zu extrahieren.

Die Hintergrundgrafik befindet sich in der angehängten Datei, zusammen mit den oben benutzten Schriftarten.

$TextFarbe1 = imagecolorallocate( $Bilddatei, 0, 125, 0 );

$TextFarbe2 = imagecolorallocate( $Bilddatei, 130, 70, 90 );

$TextFarbe3 = imagecolorallocate( $Bilddatei, 180, 90, 190 );

Jeder der weiter oben erzeugten Teilstrings soll seine eigene Farbe in der Grafik erhalten. Zu diesem Zweck wird mit imagecolorallocate() die Farbe festgelegt und in einer Variablen abgelegt. Diese Funktion erwartet 4 Parameter, die zum einen die Grafik nennt auf die die Farbe angewendet werden soll und die 3 numerischen Werte stellen die Farbwerte im RGB Modus dar. Die Farben sollten so gewählt werden das sie einen ähnlichen Ton wie die Farben der Hintergrundgrafik aufweisen. Dies erschwert zusätzlich das extrahieren des Sicherheitscode.

imagettftext( $Bilddatei, 12, 15, 3, 24, $TextFarbe1, $Schriftarten[0], $ZufallString1 );

imagettftext( $Bilddatei, 16, 0, 26, 15, $TextFarbe2, $Schriftarten[1], $ZufallString2 );

imagettftext( $Bilddatei, 14, -20, 53, 18, $TextFarbe3, $Schriftarten[2], $ZufallString3 );

Jetzt wird es zum ersten und einzigen mal etwas komplizierter in diesem Script. Die Funktion imagettftext() zeichnet Text auf die erzeugte Grafik und das geschieht für jeden Teilstring ein mal. Erwartet werden 8 Parameter die im einzelnen sind:

Die erzeugte Grafikdatei

Die Text-Schriftgröße. Kann für jeden Teilstring separate eingestellt werden.

Der Neigungswinkel der Schrift. Positive Werte neigen die Schrift nach links, negative Werte neigen die Schrift nach rechts.

Startposition des Textes von links. Ausgangspunkt ist 0 (Null) was die linke Kante der Grafik darstellt.

Startposition des Textes von oben. Ausgangspunkt ist 0 (Null) was die obere Kante der Grafik darstellt. Somit ergibt sich aus diesem und dem vorherigen Punkt das 0, 0 die linke obere Ecke der Grafik kennzeichnet.

Die Textfarbe die wir unter imagecolorallocate() festgelegt haben.

Die zu benutzende Schriftart für den Teilstring.

Der eigentliche Text der auf die Grafik soll.

Mit der Schriftgröße, der Positionierung und Neigung der Schrift ist etwas experimentieren angesagt. Man bekommt aber sehr schnell den Bogen raus und erzielt rasch das gewünschte Ergebnis.

imagejpeg( $Bilddatei );

Durch diese kleine Funktion wird die fertige Grafik an den Browser geschickt. In diesem Beispiel wird als Parameter nur die Grafikdatei angegeben. Tatsächlich aber gibt es noch 2 weitere, optionale Paramter. So kann man einen Dateiname angeben, was die erzeugte Grafik nicht an den Browser schickt sondern auf die Festplatte speichert. Ausserdem kann man noch einen Komprimierungsgrad für die erzeugte Grafik angeben um Resourcen zu sparen. Wollten wir die erzeugte Grafik nicht ausgeben sondern als JPG mit 75% Qualität speichern, würden wir imagejpeg( $Bilddatei, „captcha.jpg“, 75 ); verwenden.

imagedestroy( $Bilddatei );

Da die Grafik bereits an den Browser gesendet wurde hat sie ihre Schuldigkeit getan. Deswegen zerstören wir die erzeugte Grafik im Arbeitsspeicher und geben die dadurch belegten Resourcen wieder frei.

Das wars…!

Das war schon alles was nötig ist um einen Sicherheitscode als Grafik zu erzeugen. Gar nicht mal so schwer, oder?! :)

Alles was jetzt noch fehlt sind einige Zeilen Code im Kontaktformular und dann haben wir es auch schon geschafft.

Kontaktformular weiter ausbauen

Das Listing des Kontaktformulars vom Anfang wird nun erweitert um die Ausgabe der soeben erzeugten Grafik und um eine einfache Abfrage ob der vom Benutzer eingegebene Sicherheitscode auch korrekt ist. Die ergänzten Teile sind im folgenden Listing rot hervorgehoben.

<?php

// Session starten

session_start();

// Variablen initialisieren

$NameFehler = $EmailFehler = $NachrichtFehler = $CaptchaFehler = FALSE;

$Fehlerfrei = TRUE;

// Formular wurde abgeschickt

if (isset( $_POST['senden'] ))

{

// Felder auf Inhalt pruefen

if (strlen( trim( $_POST['name'] ) ) < 3)

{

$Fehlerfrei = FALSE;

$NameFehler = TRUE;

}

if (strlen( trim( $_POST['email'] ) ) < 3)

{

$Fehlerfrei = FALSE;

$EmailFehler = TRUE;

}

if (strlen( trim( $_POST['nachricht'] ) ) < 3)

{

$Fehlerfrei = FALSE;

$NachrichtFehler = TRUE;

}

if (md5( $_POST['sicherheitscode'] ) != $_SESSION['captcha_code'])

{

$Fehlerfrei = FALSE;

$CaptchaFehler = TRUE;

}

// Wenn alle Felder ausgefuellt wurden und der Captcha-Code korrekt war

if ($Fehlerfrei)

{

// Code zum Email-Versand ausfuehren

echo "<h2>Das Formular wurde korrekt ausgefuellt!</h2>";

}

}

?>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="de" lang="de">

<head>

<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />

<title>Kontaktformular mit Captcha</title>

</head>

<body>

<form id="kontaktformular" action="<?php echo $_SERVER['PHP_SELF']; ?>" method="post">

<fieldset>

<legend>Kontaktformular</legend>

<label for="name"><?php echo $NameFehler ? '<span style="color: #FF0000;">Ihr Name:</span>' : 'Ihr Name:'; ?></label>

<input name="name" id="name" type="text" value="<?php echo $_POST['name']; ?>" />

<label for="email"><?php echo $EmailFehler ? '<span style="color: #FF0000;">Ihre Email:</span>' : 'Ihre Email:'; ?></label>

<input name="email" id="email" type="text" value="<?php echo $_POST['email']; ?>" />

<label for="nachricht"><?php echo $NachrichtFehler ? '<span style="color: #FF0000;">Ihre Nachricht:</span>' : 'Ihre Nachricht:'; ?></label>

<textarea rows="6" name="nachricht" id="nachricht"><?php echo $_POST['nachricht']; ?></textarea>

<img src="captcha.php" alt="Sicherheitscode" title="Sicherheitscode" width="80" height="25" />

<label for="sicherheitscode"><?php echo $CaptchaFehler ? '<span style="color: #FF0000;">Bitte Sicherheitscode eingeben:</span>' : 'Bitte Sicherheitscode eingeben:'; ?></label>

<input name="sicherheitscode" id="sicherheitscode" type="text" />

<input name="senden" id="senden" type="submit" value="Absenden" class="button" />

</fieldset>

</form>

</body>

</html>

Erklärung der Erweiterungen

Zunächst wird eine Session gestartet, damit wir unterwegs nicht den Sicherheitscode verlieren der vom Captcha-Script kommt. Dann nehmen wir einfach nur die Variable $CaptchaFehler in die Initialisierung auf, die uns mitteilt ob der Sicherheitscode korrekt oder fehlerhaft war.

if (md5( $_POST['sicherheitscode'] ) != $_SESSION['captcha_code'])

{

$Fehlerfrei = FALSE;

$CaptchaFehler = TRUE;

}

Hier wird geprüft ob der vom Benutzer eingegebene Sicherheitscode mit dem Code in der Session übereinstimmt. Liegt ein Fehler vor wird dies optisch im Script angezeigt und durch das neue laden der Seite wird ein neuer Sicherheitscode erzeugt.

Im HTML-Teil wurde einfach nur ein <img>-TAG und ein weiteres Textfeld eingefügt in das der Benutzer den Sicherheitscode schreibt. Dem aufmerksamen Leser wird jetzt eine Kleinigkeit aufgefallen sein…?

<img src="captcha.php" alt="Sicherheitscode" title="Sicherheitscode" width="80" height="25" />

Wie hier zu sehen ist wird als src keine Grafikdatei angegeben, sondern der Name das Captcha-Script! Da wir im Captcha-Script aber eine Anweisung stehen haben das es sich bei der Script-Antwort um eine JPG Datei handelt, wird hier korrekterweise die Grafik angezeigt.

Wie man sieht muß man kein PHP Profi sein um seine Scripts für sich und andere vor Spam-Bots zu schützen. Der Vorteil von so einem Captcha-Script ist, daß man es nur ein mal erstellt und kann es in all seinen Scripts benutzen wo man Spam-Bots fürchten muß. So wie oben demonstriert kann man bereits bestehende Kontaktformulare, Gästebücher, usw., auch nachträglich binnen weniger Minuten und mit einfachen Mitteln erweitern um Spam-Bots fern zu halten.

Benutzte PHP-Funktionen

  • header() (http://www.php.net/manual/de/function.header.php)
  • imagecolorallocate() (http://www.php.net/manual/de/function.imagecolorallocate.php)
  • imagecreatefrompng() (http://www.php.net/manual/de/function.imagecreatefrompng.php)
  • imagedestroy() (http://www.php.net/manual/de/function.imagedestroy.php)
  • imagejpeg() (http://www.php.net/manual/de/function.imagejpeg.php)
  • imagettftext() (http://www.php.net/manual/de/function.imagettftext.php)
  • md5() (http://www.php.net/manual/de/function.md5.php)
  • session_start() (http://www.php.net/manual/de/function.session-start.php)
  • str_shuffle() (http://www.php.net/manual/de/function.str-shuffle.php)
  • substr() (http://www.php.net/manual/de/function.substr.php)
  • unset() (http://www.php.net/manual/de/function.unset.php)