User-Eingaben überprüfen
PHP-Anwendungen sichern
Risiko: Formulare
User-Eingaben überprüfen
Interaktivität ist eine der tragenden Säulen des Web. Ob Webshops, Auktionen, Diskussionsforen oder Weblogs: Ohne Interaktion zwischen Surfern und der dahinter stehenden Web-Applikation wäre keines dieser Elemente zu realisieren.
Die eigentliche Schnittstelle bilden dabei Formulare. Das Konzept dahinter ist einfach: Der Server stellt ein Formular zur Verfügung, in dem ein Benutzer die gewünschten und erforderlichen Eingaben vornehmen und diese an der Server zurückschicken kann. Dort werden dann die Eingaben weiter verarbeitet. Dieses Prinzip liegt allen Web-Applikationen zu Grunde, unabhängig davon, in welcher Sprache sie realisiert sind.
Nicht zuletzt ihrer unkomplizierten und einfachen Formularverarbeitung verdankt die Skriptsprache PHP ihren Erfolg. Die über Formulare angebotene Schnittstelle birgt natürlich auch Gefahren. Nicht immer verhalten sich die User beim Ausfüllen eines Formulars so, wie sich der Designer und Entwickler das gedacht hat.
Fehler können sich unbeabsichtigt einschleichen, zum Beispiel durch einfache Vertipper und unklare Bedienerführung. Dies stellt die eher unkritische Schwachstelle der Formularverarbeitung dar.
Ungleich gefährlicher ist der gezielte Missbrauch dieser Schnittstelle durch böswillige Angreifer, die dieses Einfallstor gezielt zum Umfunktionieren oder Zerstören der dahinter liegenden Anwendung benutzen. Die Techniken, die dabei zum Einsatz kommen, sind zum Beispiel SQL-Injection, Cross-Site-Scripting oder Cookie-Poisoning.
Angriff per SQL
User-Eingaben überprüfen
Bei SQL-Injection wird beispielsweise der Datenbank fremder SQL-Code injiziert, indem bei Abfrage-Feldern entsprechende Daten eingegeben werden. Nun ist es höchst unwahrscheinlich, dass ein Angreifer Ihr System gut genug kennt, um sofort die passenden Parameter wie Namen von Tabellen und Feldern parat zu haben. Andererseits gibt es aber auch genügend Möglichkeiten, fündig zu werden. So können die Informationen per Social Engineering in Erfahrung gebracht werden, vielleicht ist auch der Quellcode der Server-Seiten (ASP, Perl, PHP et cetera) einsehbar. Und auch mit viel Geduld lässt sich einiges erreichen. Offene Systeme sind bei Fehleingaben häufig sehr gesprächig und zeigen ganze Stack-Traces und ausführliche Systemfehlermeldungen an. Das ist fürs Fehlersuchen hilfreich, gibt Angreifern aber ebenfalls gute Tipps. Und da in der Regel Tabellen und Feldnamen häufig einem bestimmten Schema folgen, kann sich ein Angreifer sukzessive an die korrekten Werte heranpirschen.
Wenn Sie jedoch einige wichtige Punkte beachten, wird eine Attacke per SQL-Injection auf Ihrer Website kaum erfolgreich sein:
– Schützen Sie die Eingabefelder vor untergeschobenem SQL-Code, indem Sie dort, wo kein Klartext nötig ist, nur codierte Hash-Werte auf dem Server erzeugen.
– Validieren Sie auf dem Server die übertragenen Werte der Eingabefelder und sperren Sie bestimmte Zeichen und Zeichenfolgen wie SELECT, INSERT, DROP oder das Semikolon, die für SQL-Befehle notwendig sind.
– Stellen Sie Fehlermeldungen so ein, dass die Anwender keine Interna des Systems, insbesondere der Datenbank, angezeigt bekommen.
– Schützen Sie durch korrekte Webserver-Einstellungen Ihren Sourcecode vor dem direkten Auslesen.
– Fangen Sie penetrante Angriffsversuche ab, indem Sie zyklisch wiederkehrende Fehleingaben über dieselbe IP-Adresse stetig verzögern oder gar der IP-Adresse für einen bestimmten Zeitraum den Zugriff ganz sperren.
Ungeschützte Cookies
User-Eingaben überprüfen
Der blumige Ausdruck Cookie-Poisoning beschreibt eine weitere Angriffsart, Daten auf Browserseite zu manipulieren und einem Server unterzujubeln. Dies geschieht vor allem bei unzureichend geschützten Cookies. Dazu gibt es ein berühmtes Beispiel: In einem Online-Shop wurden die Warenkorb-Daten samt Preisen unverschlüsselt in Cookies gehalten und konnten einfach manipuliert werden. Die Angreifer räumten sich mit dieser Methode einen umfangreichen Rabatt ein. So etwas kann nur funktionieren, wenn die Werte eins zu eins ohne weitere Prüfung übernommen werden. Wenn auch heute die meisten Webshops diesen Fehler nicht mehr machen, ist eine solche Gefahr – gerade bei selbst aufgesetzten Systemen – durchaus noch gegeben.
Dies zeigt besonders deutlich, dass Cookies nur für unbedingt notwendige Daten eingesetzt und dort die Daten immer verschlüsselt abgelegt werden sollten. Wird dazu noch ein Plausibilitäts-Check bei der Verwendung der aus einem Cookie ausgelesen Daten durchgeführt, wird die Angriffsfläche deutlich kleiner.
Gefahr durch Javascript
User-Eingaben überprüfen
Andersherum funktioniert das Cross-Site-Scripting (XSS). Hier wird nicht versucht, über den Browser einen Server zu manipulieren, sondern der Datendieb versucht, durch geschickt platzierten Code Benutzerdaten aus dem lokalen System, auf dem der Browser läuft, zu erspähen.
Grundsätzlich geht es hierbei darum, einem Browser in einer HTML-Seite Javascript-Code unterzuschieben. Denn Javascript hat Zugriff auf das aktuelle Cookie und das aktuelle Dokument mit allen dort vorhandenen Formularfeldern. So ist es möglich, mit dieser Methode Passwortfelder auszulesen oder aus einem Cookie wichtige Informationen abzugreifen, bis hin zur feindlichen Übernahme der gesamten Verbindungs-Session. Wenn Letzteres gelingt, kann ein Angreifer unter Ihrer Flagge segeln – das kann fatale Folgen haben.
Die Angriffe können dabei direkt von maliziösen Servern kommen, die sich seriös geben. Perfider ist allerdings das Verstecken der Angreifer-Skripts in harmlosen und vertrauenswürdigen Seiten. Das können frei zugänglichen Systeme wie Gästebücher, Diskussionsforen, Auktionsserver oder Seiten mit öffentlichen Feedback-Funktionen sein. Es muss nur die Möglichkeit existieren, von Benutzerseite her einen Text eingeben zu können, der auch von anderen gelesen werden kann.
Dann lässt sich neben dem eigentlich harmlosen Text auch Javascript-Code unterbringen, der häufig gar nicht ausgefiltert wird und somit beim nächsten Aufruf der nun manipulierten Seite vom nächsten Anwender geladen wird. Der Browser nimmt so an, dass das Javascript von einer vertrauenswürdigen Seite stammt, und führt das Skript aus. So könnte nun das Skript zum Beispiel Daten auslesen und einfach an einen anderen Server weiterleiten, wo sie dann vom Angreifer abgeholt und ausgewertet werden könnten.
Es existiert noch eine andere Methode, einem Browser vorzutäuschen, ein untergeschobenes Skript käme von einer bekannten Site: Innerhalb eines URLs können ganze Skripts als Parameter mitgegeben werden. Diese Parameter werden beim Aufruf des URLs vom angesprochenen Server ausgelesen, der daraus eine Antwortseite generiert, die dem Browser zurückgeliefert wird. Existiert serverseitig ein Schwachpunkt für einen XSS-Angriff, kann es so passieren, dass das Skript einfach wieder an den Browser durchgereicht wird, der das Skript nun ausführt.
Leider kann man sich browserseitig nur gegen solche Attacken schützen, wenn man Javascript deaktiviert. Dann funktionieren die meisten Webseiten aber nicht mehr korrekt. Eine Vermeidung der Schwachstellen kann daher nur auf Serverseite geschehen:
– Stellen Sie Formulare in Ihren Web-Applikationen so ein, dass bei Benutzer-Eingaben keine Skript-Tags eingegeben werden können.
– Vermeiden Sie Cookies, wo Sie können.
– Werten Sie in URLs nur Parameter aus, die Sie kennen und benötigen. Sorgen Sie dafür, dass keine Parameter eins zu eins an den Browser weitergereicht werden.
Auch wenn sich diese Tipps wie Binsenweisheiten anhören – gerade bei einfachen Web-Applikationen mit PHP oder Perl können sich, besonders wenn man fertige Module einsetzt, solche Probleme schnell einschleichen.
Schutz durch Eingabeprüfung
User-Eingaben überprüfen
Es gibt nur eine Lösung, um Skripts sicher auszuführen: Trauen Sie keinem Benutzer. Obwohl das entmutigen mag, ist es vollkommen wahr. Benutzer können Ihre Site nicht nur »hacken«, sondern auch zufällig merkwürdige Dinge tun. Es liegt in der Verantwortung des Programmierers, sicherzustellen, dass diese unvermeidlichen Fehler keinen schwerwiegenden Schaden anrichten können. Daher müssen Sie einige Techniken einführen, die den Benutzer vor Unheil bewahren.
Eine wichtige Technik zum Schutz Ihrer Website vor Benutzern ist die Eingabeüberprüfung, ein eindrucksvolles Wort, das tatsächlich nicht viel bedeutet. Der Ausdruck besagt einfach nur, dass Sie jegliche Eingabe, die von einem Benutzer stammt, überprüfen müssen, seien es Cookies, GET– oder POST-Daten.
Deaktivieren Sie zunächst register_globals in php.ini und setzen Sie error_level auf den höchstmöglichen Wert (E_ALL | E_STRICT). Die Einstellung register_globals beendet die Registrierung von Abfragedaten (Cookie, Session, GET– und POST-Variablen) als globale Variablen im Skript; die hohe Einstellung für error_level aktiviert die Benachrichtigung für nicht initialisierte Variablen.
Sie können verschiedene Verfahren für verschiedene Eingabearten verwenden. Wenn Sie zum Beispiel einen mit der HTTP-GET-Methode übergebenen Parameter als Ganzzahl erwarten, sorgen Sie dafür, dass er eine Ganzzahl ist:
$product_id = (int) $_GET['prod_id'];
?>
Alles andere als eine Ganzzahl wird in 0 umgewandelt. Doch was geschieht, wenn $_GET [‘prod_id’] nicht existiert? Sie werden benachrichtigt, da Sie die Einstellung error_level erhöht haben. Ein besseres Verfahren zum Überprüfen der Eingabe ist:
if (!isset($_GET['prod_id'])) {
die ("Error, product ID was not set");
}
$product_id = (int) $_GET['prod_id'];
?>
Mehrere Variablen Prüfen
User-Eingaben überprüfen
Wenn Sie jedoch eine Vielzahl von Eingabevariablen haben, ist es mühselig, diesen Code für jede Variable gesondert zu schreiben. Stattdessen könnten Sie für diese Aufgabe eine benutzerdefinierte Funktion erstellen, wie im folgenden Beispiel gezeigt ist:
function sanitize_vars(&$vars, $signatures, $redir_url = null)
{
$tmp = array();
/* Die Signaturen durchlaufen und zum temporären Array * $tmp hinzufügen */
foreach ($signatures as $name => $sig) {
if (!isset($vars[$name]]) && isset($sig['required']) && $sig['required'])
{
/* umlenken, falls die Variable in dem Array nicht existiert */
if ($redir_url) {
header("Location: $redir_url");
} else {
echo 'Parameter $name not present and no redirect URL';
}
exit();
}
/* Typ auf die Variable anwenden */
$tmp[$name] = $vars[$name];
if (isset($sig['type'])) {
settype($tmp[$name], $sig['type']);
}
/* Funktionen auf die Variable anwenden.*/
if (isset($sig['function'])) {
$tmp[$name] = {$sig['function']} ($tmp[$name]);
}
}
$vars = $tmp;
}
$sigs = array(
'prod_id' => array('required' => true, 'type' => 'int'),
'desc' => array('required' => true, 'type' => 'string',
'function' => 'addslashes'));
sanitize_vars(&$_GET, $sigs, "http:// {$_SERVER['SERVER_NAME']}/ error.php?cause=vars");
?>
Schwieriger wird die Sache natürlich, wenn in einem Eingabefeld Strings ausdrücklich erlaubt sind. Namen, Orte, E-Mail-Adressen, aber auch Foren- oder Blog-Beiträge sind klassische Beispiele dafür.
Hier helfen die berühmten Regular Expressions weiter. Damit kann man sehr filigran Strings analysieren und gegebenenfalls auch modifizieren.
if (ereg("^[a-z]+\.html$", $id)) {
echo "Eingabe OK";
} else {
die("Wohl ein Hackversuch.");
}
?>
Dieses Skript checkt, ob die Variable $id einen Dateinamen enthält, der mit einem Kleinbuchstaben beginnt und mit der Extension HTML endet.
Variablen initialisieren
User-Eingaben überprüfen
Eine Besonderheit von PHP und Quelle vieler Sicherheitslücken ist die Tatsache, dass Variablen nicht initialisiert werden müssen. Zusammen mit Register-Globals oder auch einem gewollten Zugriff auf externe Daten können da schnell Probleme auftreten. Wenn Sie globale Variablen einsetzen, typischerweise als Config-Variablen, sollten Sie diese immer initialisieren. Stellen Sie sicher, dass die Variable nicht von außen manipuliert werden kann. Ein Negativbeispiel sieht etwa so aus:
$GLOBALS['puffer'] .= "Etwas Text";
?>
Wenn diese Variable nicht initialisiert wurde und dies der erste Zugriff ist, kann bei register_globals =on über test.php?puffer=evil eigener Text vorgegeben werden.
Wenn Sie Dateien auslagern, stellen Sie sicher, dass diese nicht aufgerufen werden können. Dies gilt auch für scheinbar harmlosen Dateien, die nur Variablen initialisieren. Tun Sie dies immer als Erstes, wenn Sie eine Datei anlegen. Eine zentrale Datei könnte beispielsweise so aussehen:
include("bib1.php");
include("bib2.php");
?>
Für bib1.php und bib2.php sollten Sie den Direktzugriff von Usern ausschließen. Das gebräuchlichste Verfahren dafür sieht so aus:
if( stristr($_SERVER['PHP_SELF'], "bib1.php") )
die("No direct access");
?>
Die Variable PHP_Self ist aber unsicher und kann unter Umständen verändert werden. Sauberer ist es, als Konstante eine Prüfung einzubauen, wobei später nur die Konstante abgefragt wird:
define("_ISLOADED",1);
include("bib1.php");
include("bib2.php");
?>
Damit ist die Prüfung nun einfacher:
if( !defined(_ISLOADED) )
die("No direct access");
?>
Die Regel »Trauen Sie keinen Benutzereingaben« ist profan, aber gewissermaßen die Grundregel sicheren PHP-Programmierens. Gehen Sie niemals davon aus, dass Formulare und Abfragen wie gewünscht genutzt werden. Alle externen Daten sind Sicherheitsrisiken. Machen Sie keine Ausnahmen – jede Ausnahme ist ein potenzielles Risiko.
Fertige Routinen
User-Eingaben überprüfen
Eine Menge Programmierarbeit kann man sich ersparen, wenn man für die Formular-Programmierung auf fertige Bibliotheken zurückgreift. HTML Forms Generation and Validation (www.phpclasses.org/formsgeneration) bietet zum Beispiel eine PHP-Klasse, mit deren Hilfe sich HTML-Formulare generieren lassen. Diese umfassen eine ganze Reihe von Validierungen der Eingaben, sowohl serverseitig als auch Client-seitig. Attacken auf das System über Formulare lassen sich damit leicht abwehren. Feldinhalte können mit Hilfe von Regular Expressions beliebig gefiltert werden. Über selbst entwickelte Plug-in-Klassen lässt sich die Anwendung beliebig erweitern. Die Ausgabe des Formulars kann via HTML mit eingebettetem PHP-Code oder mit Hilfe der Template-Engine Smarty gesteuert werden.