Sichere Formulare mit PHP – Teil 2/2

Im ersten Teil zum Thema sichere Formulare mit PHP wurde gezeigt, wie man Formulare auf Webseiten vor Sicherheitslücken, wie Cross-Site-Scripting, Full Path Disclosure und Mail-Header-Injection schützt.

In diesem zweiten Teil geht es um File Uploads und Captchas. Anhand von einfachen Beispielen wird gezeigt, wie man Upload-Formulare absichert, um unerwüschte Dateien fern zu halten und worauf man dabei achten sollte.

Schutz vor Uploads von unerwünschten Dateien

Bei Upload-Formularen sollte man besonders auf eine ausreichende Validierung achten. Angreifer versuchen häufig eigene Scripte (wie z.B. Webshells) hochzuladen, mit denen sie u.a. Kontrolle über sämtliche Dateien des Webservers erlangen können.

In dem Beitrag über Local / Remote File Inclusion wurde bereits gezeigt, wie versierte Angreifer eigenen Code einschleusen können. Bei einem ungeschützten Upload-Formular haben es (auch unerfahrene) Angreifer viel einfacher, da sie gar nichts einschleusen brauchen.

Hierzu ein kleines Beispiel:

<?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 festgelegt, wohin die Datei verschoben werden soll. In diesem Beispiel im Verzeichnis uploads/ unter dem selben Dateinamen.

Hinweis: Das angegebe Verzeichnis muss ausreichende Schreibrechte haben, damit die Dateien hochgeladen werden können.

Da hier keine Überprüfung stattfindet, können beliebige Dateien hochgeladen werden. Um das zu verhindern, sollte man 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 jederzeit erreichbar oder werden sie nur temporär gespeichert und als Dateianhang per E-Mail verschickt und anschließend gelöscht?

In diesem Beispiel sollen ausschließlich Bilder hochgeladen werden, die anschließend auf einer Webseite dargestellt werden sollen. Ein klassicher Bilderupload also.

Maximale Dateigröße bestimmen und überprüfen

Als erstes wird die erlaubte Dateigröße überprüft. Dazu schaut man sich 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 mittels 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 eigentlich ganz ok. Ist dort ein höherer Wert angegeben, sollte man ihn ändern. Wer keinen Zugriff auf die php.ini hat, kann das ganze per .htaccess realisieren:

php_value upload_max_filesize 2M

Dateiendung überprüfen

Als nächstes wird die Dateiendung überprüft. Da man weiß, welche Dateiendungen Bilder haben, kann man 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: JPEG, GIF, PNG');
?>

Dateiinformationen überprüfen

Nun sollte noch überprüft werden, ob es sich um ein valides Bild handelt. Dazu eignet sich die Funktion getimagesize – auch wenn diese nicht immer 100% zuverlässig ist:

<?php
  if(!getimagesize($_FILES['datei']['tmp_name']))
    die('Ungueltiges Dateiformat!');
?>

Zudem könnte man den Inhalt der Datei überprüfen, um evtl. Code-Injection in den Bilddateien zu entdecken. Dies dient nur als zusätzlicher Schutz, um Angreifern die Ausnutzung von anderen Sicherheitslücken wie Local File Inclusion 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 alles validiert wurde, kann man nun die Funktion move_uploaded_file verwenden, um die temporäre Datei in das gewünschte Verzeichnis zu verschieben.

Um zu verhindern, dass vorhandene Dateien mit dem selben Dateinamen überschrieben werden, gibt man der hochgeladenen Datei einen automatisch generierten und eindeutigen Dateinamen. Dazu eignet sich die Funktion uniqid. Wer auf nummer sicher gehen will, verwendet dazu noch mt_rand und verschlüsselt das ganze mit md5.

Für die Dateiendung wird die zuvor definierte Variable $dateiendung verwendet:

<?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 Dateinamen beibehalten möchte 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. Entweder man realisiert soetwas selber mit PHP oder man nutzt diverse Captcha-Dienste, wie z.B. Google’s reCAPTCHA.

Rechenaufgaben und Fragen als Alternative

Nicht so sicher, aber dennoch oft ausreichend sind Rechenaufgaben oder Fragen, deren Lösungen man überprüft.

Eine Rechenaufgabe als Captcha-Alternative 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äre 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 mittels JavaScript 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:

&#53;&#53;&#32;&#43;&#32;&#53;&#50;&#32;&#61;&#32;

Auch wenn ein richtiges Captcha um einiges sicherer ist, reichen solche einfachen Alternativen gegen einfache Spambots schon aus.

Fazit

Bei Formularen auf Webseiten gibt es einiges zu beachten, damit diese kein leichtes Angriffsziel darstellen. Sämtliche Benutzereingaben sollten immer geprüft, validiert und wenn möglich begrenzt werden.

Diesen Beitrag weiterempfehlen: