Websiteentwicklung: PHP: Praktische Einführung: Benutzereingaben und Sicherheit

Bevor es nun endlich an Interaktivität geht, musst du zuvor einiges über Sicherheit wissen. Überspringe keinesfalls diesen Abschnitt! Der schwerwiegendste Anfängerfehler ist es, nicht auf die Sicherheit zu achten. Durch Sicherheitslücken können andere:

  • deine Datenbank leeren
  • Passwörter und andere Daten deiner Benutzer auslesen
  • Viren in die Seite einfügen
  • selbst "passwortgeschützte" Verzeichnisse auslesen und Dateien schreiben
  • sich als andere Benutzer ausgeben

und vieles, vieles mehr. Es gibt keinen hundertprozentigen Schutz, aber du kannst alles tun, was in deiner Macht steht.

Die goldene Regel

Bearbeiten

Die wichtigste Regel lautet:

Vertraue keiner Benutzereingabe!

Natürlich will dir nicht jeder Benutzer schaden, aber leider kann man nicht davon ausgehen, dass alle Benutzer harmlos sind. Diese Regel sagt aber noch mehr: Anstatt Benutzereingaben nach gefährlichen Zeichen zu durchsuchen, wende bei allen Benutzereingaben die gleichen Methoden an, gehe also vom schlimmst möglichen Fall aus.

Herkunft von Benutzereingaben

Bearbeiten

Die zweite Regel lautet:

Alle Benutzereingaben sind unbekannter Herkunft.

Nur weil eine Eingabe im superglobalen Array $_COOKIE steht, heißt das nicht, dass der Benutzer wirklich ein Cookie auf seinem Computer hat. Auch muss der Benutzer kein Formular ausfüllen, um einen $_POST abzusetzen. Selbst der User-Agent (die Browserkennung) muss nicht von einem Browser stammen. Tatsächlich kannst sogar du mit einem einfachen PHP-Skript ein virtuelles Formular absenden, das sich hinter einem Browser deiner Wahl versteckt.

Versuche daher erst gar nicht, dich auf JavaScript oder HTML zwecks Validierung zu verlassen. Diese Mittel sind für den Benutzer gedacht, um noch bei der Eingabe ein Feedback darüber zu bekommen, ob seine Eingabe gültig ist.

Wo landen Benutzereingaben in PHP?

Bearbeiten

Benutzereingaben schreibt PHP in die superglobalen (d.h. immer im Skript verfügbaren) Variablen $_GET, $_POST, $_COOKIE sowie in einigen Elementen von $_SERVER. Die Inhalte von $_SESSION liegen dagegen auf deinem Server. Sobald du allerdings Benutzereingaben in Variablen, in Datenbanken, Sessions oder wo auch immer abspeicherst, musst du auch diese Quellen als unsicher betrachten.

Magic Quotes und Register Globals

Bearbeiten

Die Entwickler von PHP hatten zwei Features entwickelt, die seit PHP 5.3 deprecated (veraltet) sind und in einer der nächsten Versionen entfernt werden. Sie sind in den neueren Versionen (wie sie z.B. von XAMPP mitgeliefert werden) bereits in der php.ini deaktiviert.

Register Globals

Bearbeiten

Die Direktive register_globals ist für sich genommen nicht gefährlich. Anstatt in den superglobalen Arrays werden Benutzereingaben direkt als Variablen im Code registriert. Das bringt allerdings einige Probleme mit sich, da man in PHP Variablen nicht initialisieren muss. Mit register_globals = on weiß man häufig gar nicht, ob eine Variable eine Benutzereingabe ist oder von einer Bedingung stammt.

Beachten musst du in der heutigen Zeit nichts mehr, da die Direktive standardmäßig auf off steht. Ist sie an, so schalte sie in der php.ini ab. Hast du in einer Shared-Webhost-Umgebung keinen Zugriff auf die php.ini, bitte deinen Webhoster, diese Direktive schleunigst abzustellen.

Magic Quotes

Bearbeiten

Mit den Magic Quotes wollten die Entwickler von PHP zuvorkommend sein und dabei helfen, SQL-Injections vorzubeugen. Die Magic Quotes wenden auf alle Benutzereingaben die Funktion addslashes an, die bestimmte Zeichen escaped. Das ist gut gemeint, bringt aber eine Reihe von Problemen mit sich:

  • strlen wird ein für die Datenbank falsches Ergebnis liefern
  • statt addslashes sollte man die Funktion speziell für seinen Datenbanktyp verwenden
  • das Anwenden auf alle Eingaben führt zu einer Leistungsminderung

Wenn du sicher weißt, dass dein Skript nur auf Servern mit magic_quotes = off verwendet wird (wie es seit 5.3 standardmäßig der Fall ist), brauchst du dir auch hierum keine Gedanken machen. Möchtest du allerdings portablen Code schreiben, musst du prüfen, ob die Direktive magic_quotes aktiv ist, und je nachdem die Backslashes mit stripslashes entfernen.

Den aktuellen Wert der Direktive erfährst du mittels der Funktion get_magic_quotes_gpc().

Häufige Angriffstypen

Bearbeiten

Hier können nicht alle möglichen Angriffsvektoren aufgezeigt werden, lediglich einige wenige Gefahrenstellen. Solange du dich allerdings an die Goldene Regel hältst, wirst du dir diese Gefahren denken können.

Angriff auf das Dateisystem

Bearbeiten

Alle folgenden Befehle sind gefährlich:

<?php

// Nicht nachmachen!

$file = fopen($_GET['file']); // fopen: öffnet eine Datei.. oder URL

include $_GET['page'] . '.php'; // mit NUL-Byte kann auch eine Datei eingebunden werden, die nicht auf php endet

unlink($_GET['file']); // unlink: löscht eine Datei

?>

Es gibt kaum sichere Filtermöglichkeiten für Pfadangaben. Wende stattdessen die Whitelisting-Methode an: Erstelle eine Liste von möglichen Eingaben, z.B. in einem Array oder in der Datenbank, und speichere darunter die Pfadangaben bzw. Dateinamen ab.

SQL-Injections

Bearbeiten
<?php

// Nicht nachmachen!

$query = "SELECT username FROM users WHERE username = '". $_POST['username'] . '" AND password = '" . $_POST['password'] . "'";

?>

Wird nun vom Benutzer der (vorhandene) Benutzername "Max" und als Passwort "' OR 1 = 1" übergeben, liest sich der Query so:

SELECT username FROM users WHERE username = 'Max' AND password = '' OR 1 = 1

Als Ergebnis wird der Benutzer eingeloggt, ohne das Passwort zu kennen.

Um die gefährlichen Zeichen (wie das einfache Anführungszeichen) los zu werden, gibt es mehrere Möglichkeiten.

Bei Zahlen und Booleans bietet es sich an, die Benutzereingaben zu Casten. Integers, Booleans und Floats können von Natur aus keine gefährlichen Zeichen enthalten:

<?php

$id = (int)$_GET['id'];
$price = (float)$_POST['price'];

?>

Aufpassen musst du bei Zahlen, die eine Null am Anfang haben, beispielsweise Postleitzahlen oder Telefonnummern. Das musst du allerdings bereits bei der Planung der Datenbank, und diese Daten sowohl dort als auch in PHP als Strings behandeln.

Alle Variablen, die du nicht zu Integern, Floats oder Booleans gecastet hast, musst du escapen. Dazu kannst du in der Regel die Funktion deiner Datenbank verwenden. Bei MySQL wäre dies mysql_real_escape_string:

<?php

$username = mysql_real_escape_string($_POST['username']); // Sinnvoll wäre hier auch noch trim
$password = mysql_real_escape_string($_POST['password']);

$query = "SELECT username FROM users WHERE username = '$username' AND password = '$password'";

?>

Wenn du den universalen Adapter PDO benutzt, kannst du die Methode quote verwenden. Mehr über PDO im entsprechenden Kapitel.

Verwendung von Prepared Statements

Bearbeiten

Benutzt man Prepared Statements, also vorbereitete Abfragen, kümmert sich der Datenbanktreiber automatisch um das Escapen der Zeichen. Verwendet man ausschließlich Prepared Statements und benutzt im vorbereiteten Query selbst keine Benutzereingaben, ist man vor SQL-Injections ausreichend geschützt.

Beispiel für ein Prepared Statement mit PDO:

<?php
/**
 * @var PDO $db dies ist eine Instanz eines PHP Data Object
 */
$stmt = $db->prepare("SELECT username FROM users WHERE username = :username AND password = :password");

$stmt->bindParam(':username', $_POST['username']);
$stmt->bindParam(':password', $_POST['password']);

$stmt->execute();

?>
Hinweis

MySQL unterstützt Prepared Statements ab Version 4.1. Die PHP-Funktionen mysql_* kennen diese nicht, stattdessen solltest du mysqli oder, noch besser, PDO verwenden.

Cross Site Scripting (XSS)

Bearbeiten

Beim Cross Site Scripting (XSS) wird JavaScript- oder HTML-Code eingefügt und so dein Layout zerstört oder im schlimmsten Fall ein Besucher mit gefährlichem Code (z.B. Viren) konfrontiert. Eine detaillierte Erklärung von XSS findest du im Wikipedia-Artikel Cross Site Scripting. Für den Moment reicht es, wenn du weißt, dass alle Benutzereingaben bei der Ausgabe entweder zu unschädlichen Integers, Floats oder Booleans gecastet oder mittels htmlspecialchars escaped werden müssen.

<?php

echo htmlspecialchars($_POST['text']);

?>

Die Funktion wandelt alle "&", "<" und ">" in ihre Escapesequenzen "&amp;", "&lt;" und "&gt;" um.

Auch Werte in deiner Datenbank können aus Benutzereingaben entstanden sein. Deshalb musst du auch bei Anzeige dieser die Funktion htmlspecialchars anwenden.

Cross Site Request Forgery (XSRF)

Bearbeiten

Bei Cross Site Request Forgery (XSRF) wird eine Benutzereingabe getätigt, obwohl der Benutzer gar nicht die Absicht dazu hatte. Das kann z.B. durch einen manipulierten Link oder ein fremdes Formular geschehen. Dem kannst du entgegenwirken, indem du für sensible Aktionen (in SQL gesprochen: UPDATE und DELETE) auf der Seite des Formulars oder des Links ein Token erzeugst und in der Session abspeicherst. Anschließend musst du das per Link oder Formular übergebene Token mit dem gespeicherten vergleichen.

Keine Informationen preisgeben

Bearbeiten

Wenn dein Skript für die Öffentlichkeit freigegeben wird ("Produktionsumgebung"), setze in der php.ini die Direktive display_errors auf 0 und starte den Server neu. PHP-Fehler, die möglicherweise interne Details enthalten, werden dann nicht mehr angezeigt. Wenn du eine Funktion geschrieben hast, die bei jedem Query mögliche Fehler anzeigt, mache sie von einer Konstante (z.B. "DEBUG") abhängig, die du im Produktionsbetrieb auf false setzt.

Man kann auch durch Ausschalten der Direktive expose_php in der php.ini und Umstellen der zu parsenden Dateierweiterungen auf z.B. .html PHP verstecken, aber diese "security through obscurity" (Sicherheit durch Verschleierung) wird einen ernsthaften Angriffsversuch auf dein System nicht verhindern.

Eigene Macht einschränken

Bearbeiten

Wenn ein Skript eine Funktion nicht benötigt, sollte es diese auch nicht bekommen. Wenn du z.B. nie eine Datei mit fopen oder include auf einem entfernten Server öffnest, kannst du die Direktiven allow_url_fopen und allow_url_include in der php.ini deaktivieren.

Ein weiteres Beispiel dieses Prinzips sind sensible Daten wie etwa Kontonummern. Vielleicht kennst du die Methode, dass Kontonummern dem Benutzer teilzensiert gezeigt werden (etwa 12****). Aber auch schon das PHP-Skript kann einen solch eingeschränkten Zugriff bekommen: Die Kontodaten werden in einer gesonderten Tabelle der Datenbank gespeichert, für die das Skript nur Schreibrechte hat, die zensierte Kontonummer erfährt es über eine gespeicherte Prozedur der Datenbank (Siehe Wikibook Einführung in SQL: Prozeduren).

Sessions

Bearbeiten

Für Sessions gibt es weitere Sicherheitsregeln, die du im entsprechenden Kapitel Sessions findest.