Sichere Formulare – Teil 1
Auf sehr vielen (dynamischen) Websites werden Formulare verwendet. Häufig sind sie da, um Benutzereingaben an eine Webanwendung zu übermitteln.
Da viele Sicherheitslücken in Webanwendungen durch ungefilterte bzw. nicht-validierte Benutzereingaben entstehen, sind besonders Formulare ein beliebtes Ziel von Angreifern.
In diesem Artikel wird gezeigt, wie Formulardaten sicher mit PHP verarbeitet werden, um Sicherheitslücken (und Spam) zu vermeiden (Teil 1/2).
Grundlegende PHP-Kenntnisse sollten vorhanden sein bzw. sind von Vorteil.
Basis-Formular
Folgendes Basis-Formular dient in diesem Artikel als Beispiel.
<form action="" method="post" enctype="multipart/form-data">
<label for="name">Name:</label>
<input type="text" name="name" id="name" /><br /><br />
<label for="email">E-Mail:</label>
<input type="text" name="email" id="email" /><br /><br />
<label for="url">URL:</label>
<input type="text" name="url" id="url" /><br /><br />
<label for="text">Text:</label>
<textarea cols="50" rows="10" name="text" id="text"></textarea><br /><br />
<label for="datei">Datei:</label>
<input type="file" name="datei" id="datei" /><br /><br />
<input type="submit" name="form" value="Daten absenden" />
</form>
Im Browser sieht das ganze (formatiert) so aus:

Schutz vor Cross-Site Scripting (XSS)
Wenn Benutzereingaben wieder ausgegeben werden, müssen sie vorher ausreichend gefiltert werden, um XSS zu verhindern.
Dazu eignen sich die Funktionen htmlspecialchars() und htmlentities(). Diese Funktionen wandeln bestimmte Sonderzeichen in HTML-Codes um.
<?php
if(isset($_POST['name']) && !empty($_POST['name']))
echo htmlentities($_POST['name']);
?>
Hierbei muss man beachten, dass diese Funktionen standardmäßig keine einfachen Anführungszeichen (Single Quotes) umwandeln.
Damit auch Single Quotes umgewandelt werden, muss der Modus ENT_QUOTES als Parameter an die Funktionen übergeben werden.
<?php
// ...
$name = htmlentities($_POST['name'], ENT_QUOTES);
echo "<input type='text' name='name' value='$name' />";
// ...
?>
Oft kommt es auch vor, dass im action-Attribut des Formulars die Variable $_SERVER['PHP_SELF'] verwendet wird. Diese Variable enthält den Dateinamen des aktuell ausgeführten Scripts, relativ zum Document Root.
Auch diese Variable ist anfällig für XSS. Ein Angreifer könnte z.B. beliebigen JavaScript Code direkt in der URL übergeben.
vuln.php/"><script>alert('XSS');</script>
Die Variable $_SERVER['PHP_SELF'] muss also auch vor der Ausgabe gefiltert werden.
<form action="<?php echo htmlentities($_SERVER['PHP_SELF']); ?>" method="post">
Allerdings gibt es hierbei ein weiteres Problem bei alten PHP Versionen. Durch anhängen von Slashes könnte dem action-Attribut eine externe URL zugewiesen werden. Angreifer könnten somit die eingegebenen Daten empfangen und auslesen.
Eine sichere Alternative wär hier sinnvoller. In den meisten Fällen reicht ein leerer String oder für die eigene Verwendung eine statische Angabe.
Soweit zum Schutz vor XSS auf der Webseite. Doch wie sieht es mit dem E-Mail Client aus? Die meisten E-Mail Clients unterstützen HTML-Mails. Wenn Benutzereingaben in E-Mails wieder ausgegeben werden, müssen diese auch ausreichend gefiltert werden. Allerdings nur dann, wenn es sich auch wirklich um HTML-Mails handelt.
<?php
// ...
$header = 'Mime-Version: 1.0' . "\r\n";
$header .= 'Content-type: text/html; charset=iso-8859-1' . "\r\n";
$nachricht = htmlentities($_POST['text']);
mail('empfaenger@example.com', 'Betreff', $nachricht, $header);
?>
Schutz vor Full Path Disclosure
Full Path Disclosure gehört zwar zu den “harmlosen” Sicherheitslücken, man sollte sie aber nicht unterschätzen. In dem Artikel Sicherheitslücken kombinieren habe ich bereits gezeigt, wie aus harmlosen Sicherheitslücken kritische werden können.
Durch Full Path Disclosure (oder allgemein Information Disclosure) können Angreifer den vollständigen Pfad auslesen. Manchmal müssen Angreifer sogar den Pfad kennen, damit sie andere Sicherheitslücken ausnutzen können.
In den obigen Beispielen zum Schutz vor XSS haben wir die Funktion htmlentities() verwendet. Diese Funktion erwartet als Parameter einen String. Ein Angreifer kann aber auch ein Array übergeben, was zur Fehlermeldung und somit zu Full Path Disclosure führt (solange Fehlermeldungen ausgegeben werden).
Um die Ausgabe von Fehlermeldungen zu unterbinden, gibt es mehrere Möglichkeiten.
Wer Zugriff auf die PHP-Konfigurationsdatei (php.ini) hat, kann die Direktive display_errors auf Off setzen. Dadurch werden Fehlermeldungen global unterbunden.
Ansonsten kann man die Funktion error_reporting() nutzen und die Ausgabe von Fehlermeldungen zur Laufzeit unterbinden. Dazu übergibt man der Funktion den Wert 0.
<?php
error_reporting(0);
// ...
?>
Zusätzlich könnte man in der if-Abfrage überprüfen, ob es sich um ein Array handelt oder nicht. Dazu eignet sich die Funktion is_array().
<?php
if(isset($_POST['name']) && !empty($_POST['name']) && !is_array($_POST['name']))
echo htmlentities($_POST['name']);
?>
Tipp: Während der Entwicklung / während des Debuggens sollte man immer Fehler- und Warnmeldungen ausgeben lassen. So kann man z.B. error_reporting(E_ALL) verwenden und später in error_reporting(0) umändern.
Weitere Möglichkeiten
Eine weitere Möglichkeit bietet der @-Operator. Anstatt htmlentities() verwendet man einfach @htmlentities(). Das gleiche gilt für andere Funktionen.
Ansonsten kann man auch das Type Casting nutzen und der Variable einen expliziten Datentyp (in diesem Fall string) zuweisen.
Schutz vor Mail-Header Injection
E-Mails bestehen genau wie Webseiten aus einem Header und einem Body. Im Header werden bestimmte Daten, wie z.B. die Absender-Adresse und das Datum übertragen.
Bei einer Mail-Header Injection manipulieren Angreifer (bzw. Spamer oder Spambots) den Mail-Header. Meistens wird dies ausgenutzt, um Spam-Mails zu versenden. Es ist allerdings noch weitaus mehr möglich.
In PHP gibt es für das Versenden von E-Mails die Funktion mail(). Werden ungefilterte Benutzereingaben an diese Funktion übergeben, ist Mail-Header Injection möglich, da die mail() Funktion die übergebenen Parameter ungeprüft an den Mailserver schickt.
<?php
// ...
$nachricht = htmlentities($_POST['text']);
$email = htmlentities($_POST['email']);
mail('empfaenger@example.com', 'Betreff', $nachricht, 'From: ' . $email);
?>
Da Header-Einträge in E-Mails mit einem Zeilenumbruch voneinander getrennt werden, injizieren Angreifer einen Zeilenumbruch (Hexadezimal: %0A), gefolgt von beliebigen Header-Einträgen.
Mit dem CC-Feld bzw. BCC-Feld kann eine Kopie der E-Mail an eine oder mehrere E-Mail Adressen gesendet werden. Diese Felder nutzen Spamer, um Spam-Mails zu versenden (wie am folgenden Beispiel zu sehen ist).
Ein Angreifer / Spamer könnte folgenden String im Formular als E-Mail angeben.
absender@example.com%0ABcc:empfaenger1@xy.tld, empfaenger2@xy.tld
Hier wird eine Kopie der E-Mail an empfaenger1@xy.tld und empfaenger2@xy.tld gesendet. Das BCC-Feld ermöglicht eine Blindkopie; die Empfänger sehen nicht, dass die E-Mail auch an andere Adressen gesendet wurde.
Bei Wikipedia findet man eine Liste von möglichen Header-Einträgen.
Wer genau hinschaut wird feststellen, dass der String kein Zeichen enthält, welches die Funktion htmlentities() umwandeln würde. htmlentities() bietet gegen Mail-Header Injection keinen Schutz! Wir müssen eigene Funktionen zur Filterung / Validierung schreiben.
Hierbei sollte man beachten, dass manche Systeme auch andere Zeichen als Zeilenumbruch interpretieren könnten (z.B. Carriage Return – %0D).
Wichtig: Für einen sicheren Schutz gegen Mail-Header Injection muss jeder Parameter der mail() Funktion validiert / gefiltert werden, der Benutzereingaben enthält.
<?php
setlocale(LC_ALL, 'de_DE');
function checkMailParam($val, $type)
{
$a = '/(%0A|\r|%0D|\n|%00|\0|%09|\t)/ims';
$b = '/(cc:|bcc:|from:|to:|reply-to:|subject:|sender:'.
'|content-type:|content-transfer-encoding:|mime-version:)/ims';
$blacklist = ($type != 'msg') ? $a : $b;
$val = preg_replace($blacklist, '', $val);
if($type == 'mail')
{
if(preg_match('/^[\w.+-]{1,64}\@[\w.-]{1,255}\.[a-z]{2,6}$/', $val))
return true;
}
else if($type == 'subject')
{
if(preg_match('/^[[:print:]]{3,}$/', $val))
return true;
}
else if($type == 'msg')
{
if(preg_match('/^[[:print:][:space:]]{5,}$/', $val))
return true;
}
else
return false;
}
if(checkMailParam($_POST['text'], 'msg') && checkMailParam($_POST['email'], 'mail'))
{
$nachricht = htmlentities($_POST['text']);
$email = htmlentities($_POST['email']);
mail('empfaenger@example.com', 'Betreff', $nachricht, 'From: ' . $email);
}
?>
Als erstes nutzen wir die Funktion setlocale(), damit Umlaute, etc. korrekt verarbeitet werden. Dann folgt unsere eigene Funktion checkMailParam(), in der wir die möglichen Benutzereingaben für die mail() Funktion validieren und filtern. Zuerst werden alle Zeichen, die bei einer Mail-Header Injection typisch sind, durch einen leeren String ersetzt. Anschließend werden die Daten mit Regulären Ausdrücken überprüft – z.B. ob es sich um eine gültige E-Mail Adresse handelt. Ist dies der Fall, wird true zurück gegeben.
Diese Funktion nutzen wir anschließend in einer if-Abfrage. Erst wenn die Daten validiert und gefiltert wurden, wird die mail() Funktion aufgerufen.
Ausblick auf Teil 2
Das war es soweit für den ersten Teil.
Im zweiten Teil geht es um File Uploads und CAPTCHAs. Zum Schluß werd ich dann ein fertiges Formular-Script bereitstellen (evtl. später auch als WP-PlugIn
).
