Sichere Formulare mit PHP – Teil 1/2



Auf sehr vielen (dynamischen) Webseiten werden Formulare verwendet. Oftmals sind sie da, um Benutzereingaben an eine Webanwendung zu übermitteln.

Da viele Sicherheitslücken in Webanwendungen durch ungefilterte Benutzereingaben entstehen, sind besonders Formulare ein beliebtes Ziel von Angreifern.

In diesem Beitrag wird gezeigt, wie Formulardaten sicher mit PHP validiert werden, um Sicherheitslücken (und Spam) zu vermeiden.

Grundlegende PHP-Kenntnisse sollten vorhanden sein.

Schutz vor Cross-Site Scripting (XSS)

Wenn Benutzereingaben auf einer Webseite ausgegeben werden, müssen diese vorher ausreichend gefiltert werden, um XSS zu verhindern.

Dazu eignen sich z.B. die Funktionen htmlspecialchars und htmlentities. Diese Funktionen wandeln bestimmte Sonderzeichen in HTML-Codes um. Dadurch wird sichergestellt, dass der Browser diese Sonderzeichen als Text darstellt und nicht als HTML interpretiert. Somit kann kein HTML-Markup in die Webseite eingeschleust werden, wie beispielsweise das <script>-Tag zum Ausführen von JavaScript-Code.

<?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 umwandeln.

Damit diese ebenfalls umgewandelt werden, muss der Modus ENT_QUOTES als Parameter an den Funktionen übergeben werden.

<?php
  $name = htmlentities($_POST['name'], ENT_QUOTES);
  echo "<input type='text' name='name' value='$name'>";
?>

Häufig verwenden Webentwickler die Variable $_SERVER['PHP_SELF'] im action-Attribut eines Formulars. 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 per URL übergeben.

vuln.php/"><script>alert('XSS');</script>

Die Variable $_SERVER['PHP_SELF'] muss dementsprechend auch vor der Ausgabe gefiltert werden:

<form action="<?php echo htmlentities(urlencode($_SERVER['PHP_SELF'])); ?>" method="post">

In den meisten Fällen reicht allerdings ein leerer String oder der Name der PHP-Datei, welche die Formulardaten verarbeitet:

<form action="" method="post">
<form action="dateiname.php" method="post">

Soweit zum Schutz vor XSS bei Formularen auf einer Webseite. Doch wie sieht es bei E-Mails aus? Die meisten E-Mail-Programme bzw. Dienste unterstützen HTML-Mails. Wenn Benutzereingaben in E-Mails wieder ausgegeben werden, müssen diese also auch ausreichend gefiltert werden. Allerdings nur dann, wenn es sich tatsächlich um HTML-Mails handelt (sprich wenn der entsprechende Content-type definiert wurde).

<?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 diese aber nicht unterschätzen.

Full Path Disclosure (oder allgemein Information Disclosure) ermöglicht Angreifern das Auslesen von Informationen aus Fehlermeldungen. In der Regel zielen es Angreifer auf den Pfad ab, denn dieser verrät die Verzeichnisstruktur. In einigen Fällen müssen Angreifer sogar den Pfad kennen, um andere Sicherheitslücken ausnutzen zu können.

In den vorherigen Beispielen zum Schutz vor XSS wurde die Funktion htmlentities verwendet. Diese Funktion erwartet als Parameter einen String. Ein Angreifer kann aber auch einfach ein Array übergeben, was zur Fehlermeldung und somit zu Full Path Disclosure führen kann.

Um Fehlermeldungen zu unterdrücken, 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. Eine weitere Möglichkeit ist ein Eintrag in die .htaccess-Datei eines Apache-Servers:

php_flag display_errors Off

Ansonsten kann die Funktion error_reporting mit dem Parameter 0 möglichst am Anfang eines PHP-Scripts ausgeführt werden. Dadurch wird die Ausgabe von Fehlermeldungen zur Laufzeit des Scripts unterbunden:

<?php
  error_reporting(0);
?>

Zudem könnte man in der if-Abfrage überprüfen, ob es sich um ein Array handelt. Dazu eignet sich die Funktion is_array:

<?php    
  if(isset($_POST['name']) && !empty($_POST['name']) && !is_array($_POST['name']))
    echo htmlentities($_POST['name']);
?>

Weitere Möglichkeiten

Eine weitere Möglichkeit bietet der @-Operator. Dieser wird vor den Funktionsnamen definiert. Anstatt htmlentities verwendet man also @htmlentities. Das gleiche gilt für andere PHP-Funktionen.

Des weiteren 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. Spammer 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 Funktion die übergebenen Parameter ungeprüft an den Mailserver sendet.

<?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 gezielt einen Zeilenumbruch (Hexadezimal: %0A), gefolgt von beliebigen Header-Einträgen.

Mit dem CC-Feld bzw. BCC-Feld kann beispielsweise eine Kopie der E-Mail an eine oder mehrere E-Mail-Adressen gesendet werden. Diese Felder nutzen häufig Spammer, um Spam-Mails zu versenden.

Ein Angreifer/Spammer könnte z.B. folgenden String in einem unsicheren Kontaktformular als E-Mail angeben:

absender@example.com%0ABcc:empfaenger1@xy.tld, empfaenger2@xy.tld

In diesem Beispiel 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 an weitere Adressen versendet wurde.

Wer genau hinschaut wird feststellen, dass die Benutzereingabe kein Zeichen enthält, welches die Funktion htmlentities umwandeln würde. htmlentities bietet gegen Mail-Header-Injection also keinen Schutz!

Deshalb sollte man eigene Funktionen zur Validierung nutzen. Hierbei sollte man bedenken, dass bestimmte Systeme bestimmte Zeichen als Zeilenumbruch interpretieren (z.B. Carriage Return – %0D).

Für einen sicheren Schutz gegen Mail-Header-Injection sollte jeder Parameter der mail Funktion gefiltert werden, der Benutzereingaben enthalten könnte. Hier ein Beispiel:

<?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);
  }
?>

Mit der selbst definierten Funktion checkMailParam werden mögliche Benutzereingaben für die mail-Funktion validiert. Dabei werden zuerst sämtliche 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. Erst wenn die Daten gefiltert wurden, wird die Funktion mail aufgerufen und die E-Mail versendet.

Teil 2: File Uploads und Captchas

Diesen Beitrag weiterempfehlen: