Springe zum Hauptinhalt
Universitäts­rechen­zentrum
Sicheres Programmieren mit PHP

Sicheres Programmieren mit PHP

Allgemeines

Die zentralen Webserver der TU Chemnitz www.tu-chemnitz.de und www-user.tu-chemnitz.de unterstützen die Skriptsprache PHP. Die in Dokumenten integrierten PHP-Befehle werden dabei auf dem Webserver abgearbeitet.

Sicherheitsaspekte spielen auch bei der PHP-Programmierung eine immer größere Rolle, weil unsere Server

  • von einer großen Zahl von Webautoren benutzt werden,
  • Programmierlücken erschreckend schnell ausgenutzt und missbraucht werden,
  • schützenswerte Daten enthalten, die nur Berechtigten zugänglich gemacht werden sollen.

Eine absolute Sicherheit werden wir auf den allgemeinen Webservern nicht erreichen können. Sensible Daten dürfen deshalb nur über geeignete dedizierte Server (z. B. über einen eigenen virtuellen Server) und geeignete Verschlüsselungsverfahren zugänglich gemacht werden. Aber auch dann ist sorgfältige Programmierung nötig!

In folgenden Reitern werden an Hand von Beispielen einige Hinweise zur sicheren Programmierung gegeben. Dabei sind schlecht programmierte Beispiele rot und korrigierte Beispiele grün umrandet.

Überprüfung von Variablenwerten

Vertrauen Sie keinen Werten, die über Browsereingaben, den URL oder Cookies in das PHP-Skript gelangen! Alle externen Parameter, selbst wenn sie aus versteckten Feldern <input type="hidden" ... /> oder Auswahlmenüs kommen, müssen einer Plausibilitätsprüfung unterworfen werden, bevor sie im Programm verwendet werden. Das betrifft alle Werte der Felder _GET, _POST, _COOKIE, sowie _REQUEST. Dazu bietet PHP eine Vielzahl von Möglichkeiten.

# Wert aus Formular-Eingabefeld "zahl" muss numerisch sein:
$zahl = (is_numeric($_REQUEST['zahl']) ? $_REQUEST['zahl'] : 0);

PHP hat Funktionen zur Datenfilterung, die für alle externen Werte verwendet werden sollten:

# Mini- Formular
<form action="phpfilter.html" method="POST">
  Text: <input type="text" name="text">
  E-Mail: <input type="text" name="mail">
  Zahl: <input type="text" name="zahl">
  <input type="submit">
</form>
<?php # Auswertung: Schädliche Zeichen werden entfernt:
$text filter_input(INPUT_POST'text'FILTER_SANITIZE_STRING);
$mail filter_input(INPUT_POST'mail'FILTER_SANITIZE_EMAIL);
$zahl filter_input(INPUT_POST'zahl'FILTER_SANITIZE_NUMBER_INT);
echo <<<END
Eingaben nach der Filterung:
<pre>
text = $text
mail = $mail
zahl = $zahl
</pre>
END;

Siehe auch: Prüfen von E-Mail-Adressen

Andere Beispiele finden Sie in http://www.php-faq.de/q/q-security-variablen.html

Vermeiden Sie das Einbinden von zusätzlichen Skripten und Dateien über Variablen, insbesondere wenn diese aus Bestandteilen des URLs oder Cookies bestehen. So können keine ungewollten Datenpfade durch Änderung des Variablennamens erzeugt werden. Sollte dies dennoch notwendig sein, treffen Sie Sicherheitsvorkehrungen. Betrachten Sie das folgende schlechte Beispiel:

# datei ist Bestandteil des URLs
include($_REQUEST['datei']);

Unerwünschte Effekte treten ein, wenn der URL so aussieht: .../script.php?datei=/etc/passwd

Wie kann man es besser machen? Wenn eine Variable nur einen Dateinamen im gleichen Verzeichnis wie das PHP-Skript enthalten darf (aber keinen Pfadnamen), dann reicht häufig die Prüfung auf / im Dateinamen:

if (isset($_REQUEST['datei']) && strstr($_REQUEST['datei'], '/') === FALSE) {
   # datei ist gesetzt und enthält kein /
   include($_REQUEST['datei']);
} else {
   # Fehler ...
}

Wenn Variablen nur Werte aus einer vorgegebenen Menge annehmen dürfen (z.B. aus einem <select> Menü), sollte dies abgeprüft werden:

# nur diese drei Werte sind möglich:
$seiten = array('index.html' => 1'kontakt.html' => 1,
               'lehre.html' => 1);
if (isset($_REQUEST['datei']) && isset($seiten[$_REQUEST['datei']])) {
   # datei ist gesetzt und erlaubt
   include($_REQUEST['datei']);
} else {
   # Fehler ...
}

Gründlich prüfen sollten Sie unbedingt auch die Parameter für Funktionen zum Starten von externen Programmen, wie system(), exec(), passthru(), popen() und den `Backtick` Operator. Zum Maskieren von Shell-Metazeichen (z.B. Stern oder Semikolon, die mglw. unerwünschte Effekte haben können) eignet sich die Funktion escapeshellarg().

if (isset($_REQUEST['datei']) && strstr($_REQUEST['datei'], '/') === FALSE) {
   # datei ist gesetzt und enthält kein /
   # evtl. Shell-Metazeichen maskieren:
   $datei escapeshellarg($_REQUEST['datei']);
   system("/bin/ls -l $datei");
} else {
   # Fehler ...
}

Weitere Hinweise: http://www.php-faq.de/ch/ch-security.html

Cross Site Scripting (XSS)

Unter „Cross Site Scripting“ versteht man das „Einschleusen“ von fremden HTML-Code auf eine Webseite. Damit kann ein Angreifer folgende Ziele verfolgen:

  • Falsche Anzeige der Webseite: Verunstaltung (z.B. unleserlich), Anzeige fremder Inhalte (Texte, Bilder)
  • Ausspähen von Daten, z. B. Zugangsdaten (insb. Cookies), durch Einbringen von JavaScript-Code
  • Ausführen von fremden Programm-Code (insb. JavaScript)

Betroffen von solchen XSS-Problemen sind prinzipiell alle PHP- und CGI-Programme, die externe Parameter (wie Formulareingaben) verarbeiten, z. B. Gästebücher, Such- oder Anmeldeformulare.

Betrachten wir zunächst ein schlechtes Beispiel:

<form action="…"><input name="name"  ></form>
   <?php  # Gib den eingetippten Namen aus:
      print "Hallo " $_REQUEST['name'];
   ?>

Unerwünschte Effekte treten ein, wenn der URL so aussieht: .../script.php?name=<h1>Gross-und-fett

Dann würde durch die PHP-Anweisung ausgegeben:

   Hallo <h1>Gross-und-fett

Das HTML-Tag <h1> würde die Ausgabe ganz empfindlich stören. Natürlich könnten auch noch „wüstere“ HTML-Tags (z. B. Laden externer Bilder) oder gar JavaScript-Code vom Angreifer eingeschleust und im Webbrowser des Opfers abgearbeitet werden.

Erinnern wir uns an den Merksatz – der kann nicht oft genug genannt werden:

Vertrauen Sie keinen Werten, die über Browsereingaben, den URL oder Cookies in das PHP-Skript gelangen. Alle externen Parameter, selbst wenn sie aus versteckten Feldern oder Auswahlmenüs kommen, müssen einer Plausibilitätsprüfung unterworfen werden, bevor sie im Programm verwendet werden.

Also betrachten wir für unser Beispiel den Wert für name kritisch:

<form action="…"><input name="name" ></form>
   <?php  # Gib den eingetippten Namen aus
          # evtl. HTML-Code kodieren wir mit der Funktion htmlspecialchars:
      echo "Hallo " htmlspecialchars($_REQUEST['name']);
   ?>

Wenn in diesem Fall der eingetippte Name HTML-Code enthält, wird er durch die Funktion htmlspecialchars „unschädlich“ gemacht. In obigem Beispiel wird ausgegeben:

   Hallo &lt;h1&gt;Gross-und-fett

Und das bringt einen Webbrowser nicht durcheinander – auch JavaScript wird so unschädlich gemacht. Alternativ können Sie die Funktion strip_tags einsetzen, um alle HTML- und PHP-Tags aus einer Zeichenkette zu entfernen.

SQL Injection

PHP eignet sich hervorragend, um mit Webseiten Daten aus Datenbanken anzuzeigen oder zu ändern. Aber auch hier gilt es, Sicherheitsaspekte zu beachten, will man nicht böse Überraschungen erleben.

„SQL Injection“ ist eine Technik, mit der böswillige Angreifer über Web SQL-Kommandos erstellen oder existierende verändern, um versteckte Daten sichtbar zu machen, zu verändern oder zu löschen.

Betrachten wir wieder ein schlechtes Beispiel (alle Beispiele verwenden PHP Data Objects – PDO) und ein Angriffs-Szenario:

<?php
     #Datenbankabfrage an Hand einer ID aus dem URL
     $id $_REQUEST['id'];
     $result $db->query("SELECT * from tabelle WHERE id=$id");
     # Anzeige des Ergebnisse …
?>

Erwartet wird ein URL der Form …/script.php?id=42, wodurch aus der Datenbank der Tabelleneintrag mit der id=42 gelesen wird. Was aber, wenn der URL so geschrieben wird: …/script.php?id=42+or+1=1
Die Pluszeichen wandelt der Webserver in Leerzeichen um, und der arglose $db->query() -Aufruf führt plötzlich dazu, dass alle (möglicherweise sensiblen) Daten ausgelesen werden:
SELECT * from tabelle WHERE id=42 or 1=1

Auch in diesen Fall erinnern wir uns an den Merksatz: Wir müssen alle Eingabewerte überprüfen, auch wenn das etwas Mühe macht. PHP hält dafür aber eine Menge an Möglichkeiten bereit.

Wird als Wert eine Integer-Zahl erwartet, so können Sie dies prüfen mit der Funktion ctype_digit() oder Sie setzen den Typ mittels settype() explizit auf integer:

<?php
     #Datenbankabfrage an Hand einer ID aus dem URL
     $id $_REQUEST['id'];
     settype($id'integer');   # Zwangsumwandlung in Zahl
     $result $db->query("SELECT * from tabelle WHERE id=$id");
     # Anzeige des Ergebnisse ...
?>

Glücklicherweise helfen uns Funktionen der PDO: Nutzen Sie wenn immer es geht die prepared statements (vorgefertigte Abfragen) mit Platzhalten. Bei der Ersetzung durch Werte sorgt PDO automatisch für eine sichere Kodierung der Werte.

<?php    # Vollständiges Beispiel mit PDO
  if (! isset($_REQUEST['id'])) return;  # id nicht gesetzt: Schluss

  try {
    $db = new PDO('mysql:host=www-db.tu-chemnitz.de;dbname=events;charset=utf8');
  } catch(PDOException $ex) {
    die('Keine DB-Verbindung');
  }

  # Vorbereiten mit Platzhaltern
  $query $db->prepare("SELECT titel,DATE_FORMAT(startdate,'%d.%m.%Y') as datum FROM event WHERE id=?");
  # Ausführen
  $query->execute(array($_REQUEST['id']));  # ziemlich ungefährlich

  # Ergebnisse abfragen
  while ($r $query->fetch()) {
    echo 'Titel: ' htmlspecialchars($r['titel']) .
           ' am: ' htmlspecialchars($r['datum']) . "<br />\n";
  }
?>

Die Programmier-Sorgfalt muss noch größer sein, wenn die Datenbank via PHP auch geändert werden soll. Bei INSERT- und UPDATE- Operationen muss sichergestellt werden, dass plausible Werte eingefügt werden. Besondere Vorsicht ist natürlich bei DELETE-Operationen usw. nötig. Hier unterstützen auch die prepared statements.

<?php
# DB-Verbindung aufbauen weggelassen
# Eingangswerte prüfen, z. B.
if (!isset($_POST['name'])) {
  die('Kein Wert für name angegeben!');
}
$name trim($_POST['name']);  # trim = entferne Leerzeichen o.ä. am Anfang/Ende
$len strlen($name);
if ($len || $len 31) {
  die('Name muss zwischen 3 und 31 Zeichen lang sein.');
}
# usw. für $vorname und $alter

$ins $db->prepare('INSERT INTO table (name, vorname, alter) VALUES (?,?,?)');
$res $ins->execute(array($name$vorname$alter))
           or die('Fehler beim Einfügen.');
?>

Vor einer Abfrage von Daten muss zunächst die Verbindung zur Datenbank hergestellt werden, wozu neben Servernamen auch der Datenbank-Nutzer und das Passwort anzugeben ist, z.B. für MySQL:

<?php
     #Datenbank-Verbindung herstellen - wir verwenden PDO:
     $db = new PDO('mysql:host=mysql.hrz.tu-chemnitz.de;dbname=XYZ''nutzer''geheimes passwort');
?>

Hier haben wir das Problem, dass das Passwort des Datenbank-Benutzers im PHP-Skript stehen muss. Das birgt natürlich eine Gefahr, wenn es in falsche Hände gelangt. Als Grundregel beachten Sie: Verwenden Sie einen Datenbank-Benutzer, dessen Rechte entsprechend Ihrer Anwendung limitiert sind. Wenn Sie via PHP-Skript nur SELECT-Anweisungen ausführen, verwenden Sie den DB-Nutzer, der nur leseberechtigt ist.

Mit einem speziellen Verfahren steht Web-Programmierern auf den zentralen Webservern der TU ein sicherer Weg zur Verfügung, solche Geheimnisse in Webanwendungen zu verwenden, ohne dass diese im zugänglichen Dateisystem abgelegt werden müssen.

Für Anwendungen und Projekte mit höheren Sicherheitsanforderungen sind dedizierte Webserver vorzusehen. Das URZ bietet dafür virtuelle private Server an.

Weitere Hinweise:

Versenden von E-Mails

Mit Hilfe von PHP können E-Mails versendet werden. Hier ist bei der Programmierung große Sorgfalt nötig, weil sorglos programmierte Skripts häufig und erschreckend schnell von Spam-Versendern missbraucht werden. Die Folgen mussten wir schon mehrfach spüren: Unsere Mail-Server landen auf Sperrlisten, was alle Benutzer behindert.

Schauen wir uns zunächst wieder ein schlechtes Beispiel an. Über ein Formular wird eine E-Mail-Adresse abgefragt, welche dann an einen Mailing-Listen-Server gesendet werden soll:

<form action="..." method="post">
E-Mail-Adresse: <input name="adresse" /> <input type="submit" value="Eintragen" />
</form>

<?php
   if (isset $_POST['adresse'] && $_POST['adresse'] != '') {   # Formulareingabe gesetzt?
      mail('test-join@tu-chemnitz.de''''''From: ' $_POST['adresse']);
   }
?>

Dem aufmerksamen Leser fällt sofort auf, dass die Variable $_POST['adresse'], die vom Browser (oder eben dem Angreifer) kommt, fast ungeprüft übernommen wird. Schlecht, denn genau dort ist der Angriffspunkt.

Betrachten wir die Argumente der PHP-Funktion mail(): Die ersten drei sind klar: Jeweils Zeichenketten für Empfänger, Betreff und Inhaltstext (in unserem Beispiel sind Betreff und Inhalt leer). Als viertes Argument kann eine Zeichenkette mit weiteren Kopfzeilen (Header) der E-Mail angegeben werden. Im Beispiel soll die Absender-Adresse (From:) gesetzt werden. Wenn $_POST['adresse'] eine gültige E-Mail-Adresse enthält, ist alles gut.

Ein Angreifer kann uns aber als vermeintliche Formulareingabe ganz andere Daten senden, wie z. B. egal%0ABcc:%20armes@spam.opfer,nochein@spam.opfer%0A%0AKlicken-kaufen

Dekodiert man dies (%0A ist Zeilenumbruch), ergibt sich folgende Zeichenkette, die als Kopfzeilen in die Mail eingefügt werden:

  From: egal
  Bcc: armes@spam.opfer,nochein@spam.opfer

  Klicken-kaufen

Das Ergebnis: Die E-Mail wird auch an die "...@spam.opfer"-Adressen geschickt, sogar mit Inhalt, da die zwei Zeilenumbrüche das Ende der Kopfzeilen bedeuten. Natürlich sollte die PHP-Funktion mail() dies abprüfen und verhindern, tut sie aber leider nicht.

Wir als PHP-Programmierer sind natürlich hier in der Pflicht, alle extern in unser Programm gelangenden Daten genau zu prüfen - ich wiederhole mich, nicht wahr? So könnte das für obiges Beispiel aussehen:

<form action="..." method="post">
E-Mail-Adresse: <input name="adresse" /> <input type="submit" value="Eintragen" />
</form>

<?php
   if (isset $_POST['adresse'] && $_POST['adresse'] != '') {   # Formulareingabe gesetzt?
      if (preg_match("/[\r\n]/"$_POST['adresse']) {
          # adresse enthält Zeilenumbruch: eines der Zeichen \r oder \n
          # -> nicht mit uns - das lehnen wir ab!
          exit;
      }
      mail('test-join@tu-chemnitz.de''''''From: ' $_POST['adresse']);
   }
?>

Es gibt weitere „Fallen“ beim Versenden einer E-Mail. So sollte z. B. die Absende-Adresse richtig gesetzt sein, damit auch Fehler-Mails richtig ankommen (und nicht an den Betreiber des WWW-Servers gehen). Außerdem müssen die verwendeten Zeichensätze bzw. Kodierungen gesetzt sein.

Um dies einem PHP-Programmierer abzunehmen, stellen wir für die zentralen WWW-Server der TU eine eigene E-Mail-Funktion tuc_mail() bereit, die Sie verwenden sollten. Das wird im folgenden Beispiel deutlich:

<?php
require_once('php/mail.inc');    # definiert tuc_mail()

# Ein Empfaenger
  $to 'Max Mustermann <max.mustermann@hrz.tu-chemnitz.de>';
# Bei mehreren Empfängern: array() -> wird als Bcc versendet
# $to = array('maxi.musterfrau@hrz.tu-chemnitz.de', 'max.mustermann@hrz.tu-chemnitz.de');

# Absender, muss aus *.tu-chemnitz.de sein!
  $from 'Frank Richter <frank.richter@hrz.tu-chemnitz.de>';

# Subject = Betreff, möglich: Umlaute in UTF-8 oder Latin1- (ISO-8859-1)
  $subject 'PHP-Test: äöü';

# Inhalt, auch hier Umlaute möglich
  $text 'Dies ist eine Test-Nachricht, ausgelöst durch ' .
          $_SERVER['REMOTE_ADDR'];
# Antwort an - hier nicht gesetzt:
  $reply_to '';

  $ok tuc_mail($to$from$subject$text$reply_to);

  if ($ok === TRUE)
      echo "E-Mail wurde versendet.";
  else
      echo "Fehler beim Versenden: " htmlspecialchars($ok);

?>

Die Funktion tuc_mail() verlangt fünf Argumente - jeweils Zeichenketten für Empfänger, Absender, Betreff, Inhalt, und wenn gewünscht Antwort-Adresse. Hat das Versenden funktioniert, liefert die Funktion TRUE zurück, im Fehlerfall eine Zeichenkette mit der Fehlerursache.

Unsere Funktion prüft zuerst einmal die Sicherheit der Argumente, setzt die Absende-Adresse, erkennt und kennzeichnet den Zeichensatz von Betreff und Inhalt (Latin-1 oder UTF-8). Also fast ein „Rundum-Sorglos-Paket“ zum Nulltarif. Bitte benutzen!

Datei-Upload

PHP bietet Funktionen, mit denen sich ziemlich einfach ein Datei-Upload (Hochladen von Dateien) vom Webbrowser auf den Webserver realisieren lässt. Eine Erklärung des nötigen HTML-Formulars und der PHP-Anweisungen finden Sie in der PHP-Dokumentation.

Hier folgt nur ein kritischer Ausschnitt – ein rudimentäres HTML-Formular und das Kopieren einer hochgeladenen Datei aus dem temporärem Bereich in das vorgesehene Verzeichnis:

<form enctype="multipart/form-data" action="..." method="post">
  <input name="datei" type="file" />
  <input type="submit" value="Datei hochladen" />
</form>

<?php
$upload_verzeichnis '/afs/tu-chemnitz.de/.../upload';

# Name für Upload-Element im Formular heißt 'datei'
if (isset($_FILES['datei']['name'])) {
    $dateiname $_FILES['datei']['name'];
# Dateinamen prüfen: Nur Buchstaben, Punkt, Unter- und Bindestrich erlaubt:
  if (preg_match('/^[a-zA-Z0-9._-]*$/'$dateiname)) {

  # WICHTIG: Prüfen, ob Datei schon existiert, um Überschreiben zu verhindern!
    if (file_exists("$upload_verzeichnis/$dateiname")) {
      echo "Datei " htmlspecialchars($dateiname) . " existiert schon!";
    } else {
      if (move_uploaded_file($_FILES['datei']['tmp_name'],
                             "$upload_verzeichnis/$dateiname")) {
        echo "Ok";
      } else {
        echo "Fehler: " $_FILES['datei']['error'];
      }
    }
  } else {
    echo "Fehler: Ungültiger Dateiname " htmlspecialchars($dateiname);
  }
}
?>

Trotz dieser Vorsichtsmaßnahmen bietet ein solches Verfahren natürlich ein „Einfallstor“, über das auch unliebsame Dateien, etwa Schadprogramme, in unser System gelangen können. Deshalb sind gründliche Überlegungen und sorgfältige Progammierung hinsichtlich der Sicherheit erforderlich.

Bieten Sie eine solche Upload-Möglichkeit möglichst nicht öffentlich für jedermann an, sondern erlauben Sie das nur über eine Authentisierung für Berechtigte. Hinweise zum Zugriffsschutz über Anweisungen in der Datei .htaccess finden Sie unter Apache: Zugriffskontrolle.

Es ist besonders auf das Verzeichnis zu achten, in das die hochgeladene Datei geschrieben werden soll (das gilt auch für nicht-öffentlichen Zugang zum Upload-Script!). Da dieses Verzeichnis für den betreffenden Webserver schreibbar sein muss, bestehen dafür Risiken:

  • Abgelegte Dateien können Schadsoftware enthalten (z.B. Viren).
  • Prinzipiell kann jedes auf dem Webserver laufende Programm in dieses Verzeichnis Daten schreiben (oder bei vorliegenden Rechten löschen und verändern). Das kann z. B. unbeabsichtigt passieren, aber böswillig durch Schadsoftware.
  • Werden abgelegte „Hacker-Scripte“ (PHP oder CGI) vom Webserver ausgeführt, kann weiterer Schaden entstehen (z. B. Datenmanipulation).

Aufgrund dieser Risiken ist für schreibbare Verzeichnisse besondere Sorgfalt nötig:

  • Verwenden Sie ein separates Verzeichnis, in dem keine eigenen Dateien stehen.
  • Stellen Sie nur die unbedingt erforderlichen AFS-Rechte ein, z. B. dem Webserver www-user.tu-chemnitz.de nur das Listen und Einfügen neuer Dateien erlauben (nicht aber das Lesen oder Überschreiben):
    ip:www-user li
    siehe Zugriffsschutz auf Webdokumente im AFS-Dateisystem
  • Beschränken Sie Sie den Lese-Zugriff über den Webserver auf dieses Verzeichnis mittels .htaccess!

Außerdem müssen Sie sicherstellen, dass Dateien dieses Upload-Verzeichnisses nicht direkt via Webbrowser lesbar sind:

  • Legen Sie das Verzeichnis am besten außerhalb Ihres Webbereiches (d.h. nicht unter /afs/tu-chemnitz.de/www/root oder $HOME/public_html)
  • Entfernen Sie das AFS-Leserecht für system:anyuser und die WWW-Server.
  • Oder schützen Sie das Verzeichnis durch Anweisungen in der Datei .htaccess
    # Direkten Zugriff auf alle Dateien unterbinden
    Require all denied
    
  • Wenn Sie den lesenden Zugriff auf die hochgeladenen Dateien erlauben müssen (etwa zum Austausch von Daten unter Berechtigten), müssen Sie zumindest das Ausführen von CGI- oder PHP-Skripten unterbinden. Dann schreiben Sie im .htaccess
    php_flag engine off
    RemoveHandler .cgi
    

Die Schutzmaßnahmen müssen je nach Einsatzzweck unterschiedlich sein. Lesen Sie dazu „Schreibbare Verzeichnisse: Risiken und Schutzmaßnahmen“.

Übrigens: Wenn Sie eine Upload-Fähigkeit für Ihr HOME-Verzeichnis brauchen (oder für andere Verzeichnisse, für die Sie eine Schreibberechtigung haben), können Sie auf eine fertige Lösung zurückgreifen: Benutzen Sie den Web-basierten Datei-Manager WFM des Login-Servers: https://login.tu-chemnitz.de/wfm/