Häufige PHP Anfängerfehler ========================== Programmieren ist *nicht* einfach und es gibt *vieles*, was man dabei falsch machen kann. Das gilt vor allem für Sprachen wie C, aber eben auch für Skriptsprachen, wie PHP. Meistens entstehen Probleme, die es zum Beispiel ermöglichen fremden Code auszuführen, weil Nutzereingaben nicht oder nur unzureichend geprüft werden. Deshalb zu Anfang gleich das wichtigste: „User input is *hell*!“. Benutzereingaben sind das schlimmste, was man sich als Programmierer nur vorstellen kann, denn man weiß nie, was der Nutzer eingeben wird. Noch einige Anmerkungen zu diesem Artikel. Bitte niemals die ungesicherten Beispiele in eigene Skripte übernehmen. Weiterhin werden die Daten meist aus einer ``$_GET``-Variable bezogen. Das ist ein Beispiel. Grundsätzlich müssen alle Daten, die vom Benutzer kommen überprüft werden. Dazu gehören vor allem die `superglobalen`__ Variablen: ``$_GET``, ``$_POST``, ``$_REQUEST`` und ``$_COOKIE``. Ich gehe bei allen hier genannten Angriffen davon aus, dass ``magic_quotes`` ausgeschaltet ist, was – meiner Meinung nach – bei jeder vernünftigen PHP-Installation der Fall sein sollte. Eine Skriptsprache hat schließlich nicht die Aufgabe Fehler seiner Benutzer auszubügeln. __ http://www.php.net/language.variables.predefined MySQL-Injection --------------- Eigentlich *der* Klassiker unter den Fehlern ist es Daten, die in einer MySQL-Abfrage landen, nicht zu „escapen“, also in der SQL-Sprache vorhandene Steuerbefehle, wie zum Beispiel ein Stringende ' oder ", die sich eventuell in der Nutzereingabe befinden, unschädlich zu machen. Dazu wieder ein Klassiker als Beispiel. Wir nehmen an, dass ein Passwort mit dem folgenden kleinen Skript überprüft wird. Diesen Code bitte *auf keinen Fall* so in eigene Skripte übernehmen! .. code-block:: php $query = 'SELECT * FROM test WHERE user=\'' . $_GET ['user'] . '\' AND password=\'' . $_GET ['pass'] . '\';'; if (($res = mysql_query ($query))) { echo 'ok.'; } else { echo 'failed.' . mysql_error (); } if (mysql_num_rows ($res) > 0) { echo 'logged in!'; } else { echo 'failed.'; } Der Benutzer müsste sich mit einer Anfrage, wie zum Beispiel ``?user=admin&pass=password`` anmelden. Gibt er statt dessen ``?user=' OR 1=1 -- &pass=password`` ein, würde die resultierende SQL-Abfrage lauten .. code-block:: mysql SELECT * FROM test WHERE user='' OR 1=1 -- AND password='password'; Der Server sucht nun nach Einträgen, die entweder einen leeren Benutzernamen haben *oder* für die 1=1 gilt. Das ist immer der Fall, also wird der Server alle vorhandenen Einträge zurückgeben. Das doppelte Minus mit anschließendem Leerzeichen leitet einen Kommentar ein, womit der Rest der Abfrage ignoriert wird. Resultat ist nun, dass sich ein Benutzer auch ohne Zugangsdaten einen Zugang verschaffen kann. Als Lösung kommt die Funktion `mysql_real_escape_string`__ in Frage, die alle Steuerzeichen „escaped“ oder auf deutsch „maskiert“. Dabei ist zu beachten, dass diese Funktion eine offene MySQL-Verbindung benötigt. `mysql_connect`__ muss also *vor* der Benutzung ausgeführt werden. Der korrigierte PHP-Code müsste nun so aussehen: __ http://www.php.net/mysql_real_escape_string __ http://www.php.net/mysql_connect .. code-block:: php $query = 'SELECT * FROM test WHERE user=\'' . mysql_real_escape_string ($_GET ['user']) . '\' AND password=\'' . mysql_real_escape_string ($_GET ['pass']) . '\';'; Cross-Site-Scripting -------------------- Das sogenannte Cross-Site-Scripting, kurz XSS, ist mehr für die Daten des Benutzers gefährlich, als für die auf dem Server. Dazu gleich zu Anfang ein Beispiel. Nehmen wir an, dass jemand eine Suchfunktion in seine Webseite eingebaut hat und auf der Suchseite den gesuchten Begriff anzeigen lässt: .. code-block:: php echo 'Du hast nach "' . $_GET ['search'] . '" gesucht.'; /* hier folgt die Ausgabe der Ergebnisse */ Eine normale Suche würde also mit einer Abfrage, wie ``?search=dein suchtext`` erfolgen. Der Benutzer kann aber auch nach ``?search=Fetter Text`` suchen. Das Ergebnis würde dabei so aussehen: „Du hast nach **Fetter Text** gesucht“. Es kann also beliebiger HTML-, aber auch JavaScript-Code eingeschleust werden, der dann auf dem Rechner des Benutzers ausgeführt wird. Das folgende Codebeispiel zeigt, wie man alle Cookies, die die anfällige Seite auf dem Rechner des Benutzers gesetzt hat, auslesen und sich selber „schicken“ kann. Mit den gewonnenen Daten lassen sich dann fremde Sitzungen übernehmen. .. code-block:: html Weiterhin könnte ein Angreifer einen Benutzer über einen präparierten Link auf eine gefälschte Anmeldeseite schicken, die – bis auf die Länge des Links – erst einmal nicht zu erkennen ist. PHP stellt für diesen Fall zwei Funktionen bereit. Einmal `htmlspecialchars`__. Diese Funktion wandelt alle in HTML bekannten Steuerzeichen, also ``<>'"`` in die entspechenden unschädlichen Formen um. `htmlentities`__ geht einen Schritt weiter und wandelt alle möglichen Sonderzeichen in den entsprechenden HTML-Code um. Hier sollte man darauf achten welchen Zeichensatz man für die übergebenen Daten verwendet und gegebenenfalls den korrekten Zeichensatz mit an die Funktion übergeben. Das Beispiel müsste korrekt also so aussehen: __ http://www.php.net/htmlspecialchars __ http://www.php.net/htmlentities .. code-block:: php echo 'Du hast nach "' . htmlentities ($_GET ['search']) . '" gesucht.'; /* hier folgt die Ausgabe der Ergebnisse */ Andere Skripte einbinden ------------------------ Ich habe zwar schon einen Artikel zum `Einbinden fremden Codes per include`__ geschrieben, aber auch das ist meistens ein Problem von Anfängern, weshalb ich hier nochmal darauf eingehen möchte. __ 0c.html Es ist zwar praktisch einzelne Inhalte so ein ein Seitengerüst einzubinden: .. code-block:: php /* seitenkopf */ include ($_GET ['seite']); /* seitenende */ Aber das birgt Risiken. Einmal könnte ein Angreifer PHP-Code, der auf dem gleichen Webserver liegt ausführen und dementsprechend *alles* tun, was er möchte. Das Problem liegt allerdings darin diesen Code in Form einer Datei auf dem Server zu platzieren. Die zweite, eigentlich viel verheerendere, Methode wäre, Code, der auf einem anderen Server liegt auszuführen. Das geht allerdings nur, wenn die php.ini-Einstellung ``allow_url_include`` eingeschaltet ist. Lösungsmöglichkeiten gibt es viele. Auf der eben genannten Seite habe ich das Problem mit einem regulären Ausdruck gelöst. Eine zweite Möglichkeit möchte ich trotzdem noch vorstellen. .. code-block:: php $seiten = array ('seite1' => 'seite1.php', 'unterseite1' => 'unterverzeichnis/unterseite1.php'); /* usw. */ if (isset ($seiten [$_GET ['seite']])) { include ($seiten [$_GET ['seite']]); } else { include ('standardseite.php'); } Hier werden zunächst die Seitennamen und die zugehörige Datei in einem Array definiert und, sofern ein passender Eintrag existiert, bei entsprechendem Aufruf (also zum Beispiel ``?seite=unterseite1``) eingebunden. Dateidatenbanken ---------------- Dateidatenbanken sind eine praktische Sache – vor allem für kleinere Datenbanken, bei denen MySQL zum Beispiel „Overkill“ wäre. Aber auch diese Datenbanken in Dateien sind anfällig für Angriffe. Eine Datenbank könnte so aussehen: Einzelne Werte werden mit einem ``|``, die Datensätze jeweils mit einer neuen Zeile voneinander getrennt. Die Felder sind: Benutzername, Privilegien, Name, Passwort. :: root|1|Klaus Mustermann|klauspasswort benutzer|2|Tom Mustermann|tomspasswort Die Daten werden dementsprechend mit diesen sehr einfachen Funktionen geladen und gespeichert: .. code-block:: php function loadData ($file) { $content = file ($file); $ret = array (); foreach ($content as $line) { $ret[] = explode ('|', $line); } return $ret; } function saveData ($file, $data) { $fd = fopen ($file, 'w'); foreach ($data as $line) { fwrite ($fd, implode ('|', $line) . "\n"); } fclose ($fd); } Nun benutzt aber jemand in seinem Benutzernamen ein ``|``-Zeichen, also ein „Steuerzeichen“ der Datenbank. Damit kann man die Datenbank für das Skript unbrauchbar machen, denn die Daten sind plötzlich nicht mehr die, die erwartet werden. Genauer: Die Index-Zuordnungen im Array (``$array[0]``, ``$array[1]``, …), das von loadData zurückgegeben wird, stimmen nicht mehr. Das ist zwar zunächst nicht gefährlich, aber ärgerlich. Zum Problem werden die Steuerzeichen ``|`` und ``\n`` erst, wenn jeder einen Benutzer anlegen darf. So könnte man sein Passwort wie folgt wählen, um einen weiteren Benutzer mit mehr Rechten zu schaffen: ``meinpasswort\nneuerbenutzer|1|Mehr Rechte|neuespasswort``. Die Datenbank sieht danach wie folgt aus: :: root|1|Klaus Mustermann|klauspasswort benutzer|2|Tom Mustermann|tomspasswort meinbenutzer|2|Mein Name|meinpasswort neuerbenutzer|1|Mehr Rechte|neuespasswort Die letzte Zeile ist eigentlich das eingegebene und nicht überprüfte Passwort. Jetzt übernimmt es die Funktion eines eigenen Datensatzes und ermöglicht zum Beispiel ein Anmelden mit mehr Rechten mit dem Benutzernamen „neuerbenutzer“. Deshalb ist es wichtig die eingegebenen Daten möglichst in der Funktion zum Speichern der Datenbank zu überprüfen und gegebenenfalls zu säubern. Hier reicht ein einfaches `str_replace`__ aus. __ http://www.php.net/str_replace .. code-block:: php $gereinigt = str_replace (array ("|", "\n"), '', $benutzereingabe); Eine weitere Möglichkeit wäre die PHP-Funktionen `serialize`__ und `unserialize`__ zu benutzen, die komplette Arrays und Objekte in Dateien schreiben beziehungsweise diese aus ihnen lesen können. Hier kann die Datenbank zwar nicht manipuliert werden, aber die Eingaben des Benutzers sollten vor dem Verarbeiten – wie immer – überprüft und bereinigt werden. __ http://www.php.net/serialize __ http://www.php.net/unserialize Noch ein letztes Problem, das mit Dateidatenbanken zusammenhängt: Sie können, sofern man den Dateinamen kennt, vom Benutzer angezeigt werden. Das ist im Beispiel oben problematisch, weil die Passwörter im Klartext vorliegen. Gegen das Auslesen hilft ein Eintrag in der .htaccess-Datei des entsprechenden Verzeichnisses. Passwörter sollte man aber trotzdem in *keiner* Datenbank im Klartext speichern!