„Sicherheit” mit PHP/CGI, MySQL


Einleitung

Wie wir während des ReferatPHPMySQL und bei der Schulbanker-Sache (siehe MataNews) gesehen haben ist es teilweise recht einfach eigenen Code in fremde Webseiten zu injizieren. Die folgenden Hinweise und Gedanken beziehen sich größtenteils auf PHP und MySQL, können aber auch unkompliziert auf andere Programmiersprachen für CGI-Anwendungen, wie z.B. Perl … übertragen werden.

Eins vorweg: Obwohl PHP versucht es dem Anwender möglichst einfach zu machen, sollte man doch daran denken, was man bei C alles falsch machen kann und mit dieser fast schon paranoiden Vorsicht seine Programme schreiben.

Texteingabefelder

… werden zur Ermittlung von „handgeschriebenen” Daten des Nutzers verwendet, z.B. auch hier im Wiki. Als Übertragungswege vom Client zum Server stehen dabei die HTTP-Methoden GET und POST zur Verfügung, wobei für große Datenmengen (von einem <textarea>) und für nicht offensichtliche Daten besser POST verwendet wird (ich glaube in irgendeinem RFC ist eine maximale Größe des Datenstroms angegeben). Damit ist es allerdings noch längst nicht getan. Im folgenden werde ich zuerst die Risiken ungeprüfter Benutzereingaben allgemein besprechen, danach speziell die Gefahren der Übermittlung per GET.

der einkommende Datenstrom

HTML-Injektion

In vielen Fällen – nicht nur bei privaten Gästebüchern – werden zwar die „großen” Textbrocken, also etwa der Kommentar, per PHPs htmlspecialchars() codiert, nicht aber andere Teile, die auch „so wie sie sind” später als HTML-Code an den Browser gesendet werden, etwa Name und Emailadresse. Hier liegt die Schwachstelle, nicht unbedingt für den Betreiber des Servers, aber vorallem für den ahnungslosen Nutzer, der z.B. beim Betreten des Gästebuchs tausende von Popups um die Ohren gehauen bekommt, weil über ungeprüfte Formulareingaben JavaScript-Code injiziert worden ist. Aber selbst in normalem HTML-Code können sich entsprechende Bugs verstecken, z.B. mag der Internet Explorer nicht die folgende Art eines <input> : <input type crash> Auf die weitere Einbettung aktiver Inhalte und Cookies brauche ich denke ich in diesem Zusammenhang nicht weiter einzugehen, man kann froh sein, wenn bei einem ungesicherten Gästebuch nur das Logo von einem fremden Server prangt und nicht noch irgendwelche ActiveX-Sachen …

In anderen Sprachen für CGI-Anwendungen (moderner: Webservices) gibt es u.U. keine htmlspecialchars(), doch mit regulären Ausdrücken ist es relativ einfach wichtige HTML-Zeichen zu maskieren, vorallem < > & " '. Im Zusammenhang mit PHP und der Weiterverarbeitung von Text mit Quotes sollte man zusätzlich in der php.ini folgende Einstellung treffen:

magic_quotes_gpc = On

Damit wird von außen kommenden Quotes von PHP automatisch ein Backslash vorangestellt. Will man diesen wieder loswerden: stripslashes() aber vorsichtig einsetzen:

SQL-Injektion

Gerade wenn die oben angesprochene Einstellung nicht getroffen ist, besteht die Möglichkeit Code in SQL-Abfragen zu injizieren:

$req = sprintf("INSERT INTO gb (name, email, text) VALUES " .
		"('%s', '%s', '%s')",
		$name, $email, $text);

So weit, so gut. Doch was ist nun, wenn – sagen wir text – einen etwas ungewohnten Inhalt hat:

// $text enthält
// "Toll'); UPDATE users SET (passwd = 'meins') WHERE (name='root'"

$req = sprintf("INSERT INTO gb (name, email, text) VALUES " .
		"('%s', '%s', '%s')",
		$name, $email, $text);

echo $req;

// ergibt (der Leserlichkeit halber umbrochen):
// INSERT INTO gb (name, email, text) VALUES ('ich', 'user@invalid.com', 'Toll');
// UPDATE users SET (passwd='meins') WHERE (name='root')

Man stelle sich vor, die Datenbank habe ferner kein UNDO und ich würde noch ein DROP TABLE einfügen … Aus diesem Grund ist es besser, die von PHP eingefügten Backslashes beim Schreiben in die Datenbank zu behalten und stripslashes() erst beim Auslesen zu verwenden, da man sonst das angesprochene Problem nicht eliminiert.

Übermittlung per URI/HTTP-GET

Es gibt Anwendungen, für die GET gut geeignet ist, z.B. Suchmaschinen, bei denen man dem User die Möglichkeit zur Speicherung seiner Abfrage ermöglichen möchte oder bei PHP-basierten CMS (Content Managment Systeme). Erstens gilt das gleiche wie für die verschiedenen Wege der Injektionen, man kann aber auch zusätzliche Variablen in PHP injizieren, sofern in der php.ini die Einstellung register_globals auf On steht. Wenn register_globals aktiviert ist, hat man als Programmierer zwar (etwas) weniger Aufwand die Variablen nach PHP hereinzubekommen, dadurch ergeben sich allerdings auch große Sicherheitslücken:

Nehmen wir an, wir rufen eine Seite per folgender URI auf: http://www.example.org/secret/login.php im zugehörigen Formular wurden die Werte name und passwort gesetzt. Das zugehörigePHP-Script check.php sieht so aus:

<?php
if (userExists($Name)) {
   if ($Passwort = getStoredPwd($Name)) {
         $is_a_user = true;
    }
}

// etwas später im Code ...

if ($is_a_user) {
   header("Location: very-secret.html");
} else {
   header("Location: /");
}
exit;
?>

Warum sollte man sich bei diesem Script noch die Mühe machen per Brute-Force Benutzername und Passwort zu erraten? Man kann diese zwar auch bequem per URI übermitteln, man braucht nur entsprechende Anfragen zu generieren. Viel einfacher ist es allerdings die Seite direkt mit login.php?is_a_user=1 aufzurufen. Aus diesem Grund wurden in PHP die assoziativen Arrays $_POST, $_GET sowie $_COOKIE eingerichtet. Wenn es wirklich gleichgültig ist, woher die Daten stammen, kann man immer noch $_REQUEST verwenden.

Generell sollte man Verzeichnisse mit sensiblen Daten nicht durch solche Methoden sowie „Security by Obscurity” sichern, sondern z.B. durch eine htaccess und/oder HTTPS-Übertragung. In diesem Zusammenhang möchte ich noch auf die Möglichkeit einer sogenannten robots.txt hinweisen, die unter der Document Root abgelegt wird. Damit teilt man Suchmaschinen-Robots mit, welche Verzeichnisse diese sehen und nicht sehen dürfen. Dies könnte allerdings von bösartigen Bots ausgenutzt werden.

URI-Encoding

Um Umlaute und Sonderzeichen per GET zu übertragen, werden diese nach einem bestimmten Muster codiert: Ein Prozentzeichen gefolgt von zwei hexadezimalen Ziffern, die den ASCII-Wert des Zeichens angegeben. Der Nachteil ist, dass man auch Nicht-Sonderzeichen auf diese Weise codieren kann. PHP nimmt einem die Aufgabe der Decodierung ab, sofern man auf die oben beschriebene Weise auf die Werte zugreift. Muss man hingegen direkt auf den Querystring zugreifen, sind die Zeichen nicht codiert. So können einige Prüfungen auf Werte fälschlicherweise doch positive Ergebnisse liefern. Mehr zu dieser Problematik später.

Abgesehen davon, dass mir diese ganzen Seiten, die alle Cookies setzen wollen, gehörig auf den Nerv gehen, stellt dies für gewiefte Webbenutzer kein großes Hindernis dar. Der Standardanwender „weiß” zwar, dass er den Inhalt des Cookies nicht ändern kann, der schlaue macht es einfach. Deshalb kann man sich nicht auf die Werte von Cookies verlassen. Ich möchte an dieser Stelle nur noch einmal an die Diskussion aus WiWi erinnern (geänderte Preise durch manipulierte Cookies).

Dateihandling

Für manche Anwendungen kann es unerlässlich sein, dass Dateinamen oder Teile davon per URI übergeben werden. Als abschreckendes Beispiel gilt dafür immer file.php?name=subdir/file.ext. Das mag bösartige Menschen zwar herausfordern, muss aber gar nicht so gefährlich sein: Wenn Dateinamen an eine CGI-Anwendung übergeben werden, muss beachtet werden, dass die Dateien nicht nach den Regeln des Webservers, sondern des zugrunde liegenden Systems behandelt werden. D.h. / steht für das Wurzelverzeichnis, nicht für die Document Root. Daraus ergibt sich, das Dateien entweder gar nicht oder nur kontrolliert die Server-Umgebung verlassen dürfen. Vor allem auf führende Slashes und ../ ist zu achten! Besser ist es aber die Dateinamen aus verschiedenen Parametern zusammenzubauen, z.B. file.php?category=subdir&name=file&type=ext; Achte darauf, dass das Muster dahinter nicht zu offensichtlich ist, d.h. file.php?category=/etc&name=passwd&type= darf nicht den beabsichtigten Zweck erfüllen. Andere Möglichkeiten sind Script-interne Vorgaben wie z.B. $dir = 'dieses/hier' wobei dann alle Dateien aus $dir stammen; relativ sicher ist den endgültigen Dateinamen erst im Script zusammenzusetzen, z.B.

// URI: file.php?category=sub&name=file

$filename = sprintf('%sdir/%s.ext', $_GET['category'], $_GET['name']);

Es sollte immer sichergestellt sein, dass die benötigten Werte auch tatsächlich an die Seite übermittelt wurden, es könnte zu einem komischen Verhalten führen, riefe ich file.php ohne den URI-Bestandteil name auf.

Auslesen von Dateien

Falls die Daten im HTML-Format vorliegen, kann man diese per readfile() direkt an den Browser senden. Ein include() stellt eine zusätzliche Gefahr dar und wird deshalb nur bei Modulen verwendet!

Die gleiche Überlegung gilt übrigens auch für die Verwendung von Variablen: eval() ist wirklich evil!

weitere Ein-/Ausgabe

Eine – wenn auch eher bei C – beliebte Methode ist es Variableninhalte mit der Funktion printf() auszugegen, weil puts() automatisch einen Zeilenvorschub ausführt. Was ist nun, wenn der Variableninhalt ein String ist und printf-Formatcodes enthält? PHP-Fehler werden zum Glück durch den Interpreter abgefangen, aber C-Programme werden in solchen Fällen wegen Zugriffsfehler auf Grund eines Buffer Overflows gerne vom Betriebssystem gekillt, sofern es dies bemerkt. Nichtsdestotrotz sind Buffer Overflows exploitbar, wie wir im ReferatSicherheit gesehen haben. Deshalb, wenn man printf() verwendet, dann z.B. so: printf("%s", stringvar)

Verwendung von Funktionen

Neben der oben genannten Vorsicht, gerade bei Verzeichniszugriffen, gibt es noch weitere Gefahren im Zusammenhang mit Funktionsaufrufen. Viele eingebaute Funktionen geben im Fehlerfall eine Menge an Meldungen direkt an den Browser aus. Dies können Hinweise für potenzielle Kriminelle sein, nicht nur wo auf dem Serverrechner die Homepage liegt. Deshalb gibt es bei vielen (allen?) PHP-Funktionen die Möglichkeit durch ein vorangestelltes @ die Ausgabe von Fehlermeldungen zu unterdrücken. Allerdings muss man dann auf jeden Fall die Rückgabewerte prüfen (dies sollte man sowieso tun!).
Eine weitere Funktion, die man nur bedächtig einsetzen sollte, ist mail() Wie der Name schon sagt, kann man damit Emails versenden. Durch schlampige Programmierung kann man dies ausnutzen um große Mengen Spam zu versenden. Da freut sich vor allem der Webhoster!

Verwendung von Modulen

Externe Sourcecodemodule werden als normale Dateien behandelt, d.h. für includes mit Variablen ist die gleiche Vorsicht wie beim Öffnen von Dateien notwendig. Weiterhin sollte man verschiedene Funktionalitäten in verschiedene Dateien ausgliedern, die sich die gleichbleibenden Funktionen per include holen. Der Grund ist, dass wir uns bei nur einer Datei mit vielen includes den flachen Namensraum „zumüllen”. Im Gegensatz zu einer Compilersprache konvertiert PHP einfach zwischen verschiedenen Typen, d.h. aus einem Array kann schnell ein int werden.

Include-Schutz

Mit den jetzt erworbenen Informationen über Variablen und Includes kann man relativ einfach einen Include-Schutz implementieren. Vorraussetzung ist allerdings, dass register_globals deaktiviert ist, ansonsten muss bei der Abfrage in der Includedatei zusätzlich geprüft werden, ob es nicht einen gleichlautenden Eintrag in $_REQUEST gibt.

// in der Includedatei
if (! isset($canInclude))
   die "Zugriff verweigert!";

// im Hauptprogramm
$canInclude = 1;
include(modul);