Sichere Formulare – Teil 2
In zweiten Teil des Artikels über sichere Formulare geht es um File Uploads und CAPTCHAs. Außerdem wird ein fertiges Formular-Script zum Download bereitgestellt, das ihr uneingeschränkt auf eurer eigenen Website verwenden könnt.
Schutz vor Uploads von unerwünschten Dateien
Bei Upload-Formularen sollte man besonders auf eine ausreichende Validierung achten. Angreifer versuchen oft eigene Scripte (z.B. Webshells) hochzuladen, mit denen sie Kontrolle über Dateien auf dem Webserver erlangen können.
In dem Artikel über Local / Remote File Inclusion habe ich bereits gezeigt, wie versierte Angreifer eigenen Code injizieren können. Bei einem ungeschützten Upload-Formular haben es (auch unerfahrene) Angreifer viel leichter, da sie gar nichts injizieren brauchen.
Hierzu ein kleines Beispiel (basierend auf dem Basis-Formular - siehe Teil 1).
<?php
//...
if(move_uploaded_file($_FILES['datei']['tmp_name'], 'uploads/' . basename($_FILES['datei']['name'])))
echo 'Die Datei wurde erfolgreich hochgeladen.';
?>
Die Funktion move_uploaded_file() ermöglicht den Upload. Als zweiter Parameter wird das Verzeichnis und der Dateiname angegeben, wo die Datei hin verschoben werden soll. In diesem Fall im Verzeichnis uploads/ unter dem selben Dateinamen.
Hinweis: Das angegebe Verzeichnis muss ausreichende Schreibrechte haben.
Da hier keine Überprüfung stattfindet, können beliebige Dateien hochgeladen werden. Um das zu verhindern, müssen wir erstmal wissen, welche Dateien überhaupt hochgeladen werden sollen. Bilder? Archive? Textdateien? Außerdem stellt sich die Frage, was mit den hochgeladenen Dateien angestellt werden soll. Sind sie im Web erreichbar oder werden sie irgendwo außerhalb des Document Roots auf dem Server gespeichert? Oder werden sie nur temporär gespeichert und als Dateianhang per E-Mail verschickt?
In diesem Beispiel sollen auschließlich Bilder hochgeladen werden, die anschließend auf einer Webseite dargestellt werden.
Maximale Dateigröße überprüfen / bestimmen
Als erstes überprüfen wir die erlaubte Dateigröße. Dazu schauen wir uns den Wert der Direktive upload_max_filesize in der Konfigurationsdatei php.ini an. Wer keinen Zugriff auf diese Datei hat, kann den Wert auch einfach mit PHP ausgeben lassen:
<?php
echo ini_get('upload_max_filesize')
?>
Standardmäßig sind 2 MB erlaubt (upload_max_filesize = 2M). 2 MB sind für Bilder denke ich mal okay. Ist dort ein höherer Wert angegeben, sollte man ihn ändern und wer keinen Zugriff auf die php.ini hat, sollte das ganze per .htaccess realisieren:
php_value upload_max_filesize 2M
Dateiendung überprüfen
Als nächstes überprüfen wir die Dateiendung. Da wir wissen, welche Dateiendungen Bilder haben, können wir eine White-List mit erlaubten Dateiendungen erstellen.
<?php
//...
$erlaubt = array('.jpg', '.jpeg', '.gif', '.png');
$dateiendung = strtolower(strrchr($_FILES['datei']['name'], '.'));
if(!in_array($dateiendung, $erlaubt))
die('Ungueltiges Dateiformat! Erlaubte Dateiformate: JPG, GIF, PNG');
?>
Dateiinformationen überprüfen
Nun überprüfen wir noch, ob es sich um ein valides Bild handelt. Dazu eignet sich die Funktion getimagesize() - auch wenn diese nicht zu 100% zuverlässig ist.
<?php
//...
if(!getimagesize($_FILES['datei']['tmp_name']))
die('Ungueltiges Dateiformat!');
?>
Außerdem überprüfen wir den Inhalt der Datei, um evtl. Code Injection zu entdecken. Dies dient nur als zusätzlicher Schutz, um Angreifern die Ausnutzung von anderen Sicherheitslücken (z.B. LFI) zu erschweren.
<?php
//...
$file = fopen($_FILES['datei']['tmp_name'], 'rb');
$contents = fread($file, filesize($_FILES['datei']['tmp_name']));
fclose($file);
$check = array('<script', 'javascript:', '<?php', '$_GET', '$_POST', '$_COOKIE', '$_SERVER', '$HTTP', 'system(', 'exec(', 'passthru', 'eval(', '<input', '<frame', '<iframe', 'http://');
foreach($check as $chk)
if(strpos($contents, strtolower($chk)) !== false)
die('Code Injection erkannt!');
?>
Eindeutige Dateinamen vergeben
Nachdem wir alles validiert haben, können wir nun die move_uploaded_file() Funktion verwenden, um die temporäre Datei in unser gewünschtes Verzeichnis zu verschieben.
Um zu verhindern, dass vorhandene Dateien mit dem selben Dateinamen überschrieben werden, geben wir der hochgeladenen Datei einen generierten und eindeutigen Dateinamen. Das können wir mit uniqid() erledigen. Und um auf nummer sicher zu gehen, verwenden wir noch mt_rand() und verschlüsseln das ganze mit md5().
Für die Dateiendung verwenden wir die zuvor definierte Variable $dateiendung.
<?php
//...
$dateiname = md5(uniqid(mt_rand(), true)) . $dateiendung;
if(move_uploaded_file($_FILES['datei']['tmp_name'], 'uploads/' . $dateiname))
echo 'Die Datei wurde erfolgreich hochgeladen.';
else
die('Fehler beim Hochladen der Datei!');
?>
Wer für den Upload den selben Dateinamen verwenden will und in der Funktion move_uploaded_file() die Variable $_FILES['datei']['name'] nutzt, sollte unbedingt (wie im ersten Code-Beispiel zu sehen ist) die Funktion basename() verwenden, da ansonsten eine kritische Sicherheitslücke entsteht. Angreifer könnten wichtige, vorhandene Dateien in anderen Verzeichnissen überschreiben.
Schutz vor Spam und DoS - CAPTCHAs
CAPTCHAs werden zum Schutz vor Spambots und Denial of Service (DoS) Angriffen verwendet. Wie man sowas in PHP realisiert werde ich in einem eigenen Artikel genauer erklären. An dieser Stelle möchte ich nur einmal auf Google's reCAPTCHA hinweisen.
Rechenaufgaben und Fragen als Alternative
Oft reicht es auch aus, normale Rechenaufgaben oder einfache Fragen zu verwenden.
Das könnte z.B. so aussehen:
<?php
session_start();
function newSession()
{
$_SESSION['zahl1'] = mt_rand(1, 100);
$_SESSION['zahl2'] = mt_rand(1, 100);
$_SESSION['ergebnis'] = $_SESSION['zahl1'] + $_SESSION['zahl2'];
}
if(!isset($_SESSION['ergebnis']))
newSession();
//...
if(isset($_POST['code']))
{
if((int)$_POST['code'] != $_SESSION['ergebnis'] ||
strpos($_POST['code'], '.') !== false ||
!is_numeric($_POST['code']) || is_array($_POST['code']))
{
echo 'Falsches Ergebnis!';
unset($_SESSION['zahl1']);
unset($_SESSION['zahl2']);
unset($_SESSION['ergebnis']);
newSession();
}
else
{
echo 'Richtig!';
session_destroy();
// Daten verarbeiten
exit;
}
}
$rechenaufgabe = $_SESSION['zahl1'] . ' + ' . $_SESSION['zahl2'] . ' = ';
?>
<form action="" method="post">
<?php echo $rechenaufgabe; ?><input type="text" name="code" maxlength="3" />
<input type="submit" name="form" value="Daten absenden" />
</form>
Natürlich wär es sinnvoll hier noch eine IP-Sperre nach x gescheiterten Versuchen einzubauen, da Spambots mit normalen Rechenaufgaben keine Probleme haben sollten. Allerdings kann man die Rechenaufgabe auch in Unicode oder mit JS ausgeben
<?php
function unicode($ascii)
{
$unicode = '';
for($i = 0; $i < strlen($ascii); $i++)
$unicode .= '&#' . ord(substr($ascii, $i, 1)) . ';';
return $unicode;
}
//...
$rechenaufgabe = $_SESSION['zahl1'] . ' + ' . $_SESSION['zahl2'] . ' = ';
$rechenaufgabe = unicode($rechenaufgabe);
//...
?>
Aus 55 + 52 = wird dann folgendes.
55 + 52 = 
Auch wenn ein richtiges CAPTCHA um einiges sicherer ist, reichen solche einfachen Alternativen meiner Erfahrung nach meistens aus.
Fertiges Kontaktformular - einfach und sicher
Wie versprochen habe ich ein fertiges Script geschrieben, das ihr euch hier downloaden könnt. Das ganze sieht ungefähr so aus:

Es ist nur eine Datei und läuft ohne Datenbank. Das Formular kann bequem mit einer PHP Include-Funktion in einer Webseite eingebunden werden.
Das einzige, das ihr an dem Script ändern müsst, ist eure E-Mail Adresse und wenn ihr wollt den Betreff der E-Mail.
<?php
define('EMPFAENGER', 'empfaenger@example.com'); // Deine E-Mail Adresse
define('BETREFF', 'Neue Nachricht - Kontaktformular'); // Betreff der E-Mail
?>
Ich erstelle bald eine Download Seite, da gibt's dann nochmal ne ausführliche Beschreibung. Ansonsten einfach hier per Kommentar melden


1Michael Karl kommentierte am 30. Dezember 2009 um 09:53 Uhr
Gute Arbeit
Beim Unicode bleibt allerdings hinzuzufügen, dass die Meisten Spam-Robots auch diesen inzwischen lesen und auswerten können.
Viele Grüße,
Michael