search
subnavi
Werbung

Sicheres Programmieren in PHP

Frage: Wie unterscheide ich böse Variablen von guten?

Antwort von Kristian Köhntopp:

Es ist im wesentlichen egal, wie die Daten in das lokale Programm kommen - GET, POST, COOKIE oder Request-Parameter spielt kaum eine Rolle. Es ist jedoch wesentlich, dass diese Variablen a) keine Variablen des lokalen Programmes unkontrolliert überschreiben können und b) dass der Übergang von solchen benutzerkontrollierten Variablen in lokale Variablen des Programmes nur nach einer Prüfung der Variablen auf legale Werte stattfindet. Das bedeutet:

    1. Variablen mit vorgegebenen Werten aus einer Wertemenge ("Auswahlvariablen"), wie sie zum Beispiel aus einem <select> fallen:

      Das Programm sollte ein lokales Array haben, das die möglichen Werte als Index eines Hash enthält:

      $val = array( "wert1" => 1, "wert2" => 1, "wert3" => 1 );

      Die Prüfung von $_GET["selectvar"] kann nun so aussehen:

      $checkedvar = isset($val[$_GET["selectvar"]]) ? $_GET["selectvar"] : "default";

      Wenn $_GET["selectvar"] einen in $val definierten Wert hat, dann wird dieser Wert nach $checkedvar übernommen. Hat man versucht, dem Programm einen vierten Wert unterzuschieben, dann wird $checkedvar auf "default" gesetzt. Das Programm arbeitet dann ausschliesslich mit $checkedvar. Auf diese Weise können dem Programm keine beliebigen Werte aus $_GET["selectvar"] untergeschoben werden.

    2. Variablen, die eine bestimmte Form haben müssen, aber für die nicht die abgeschlossene Wertemenge aufgezählt werden kann ("Freitextvariablen mit Formvorschrift"):

      Das Programm sollte eine oder mehrere Regex in einem Feld vorliegen haben:

      $val = array( '/^[a-zA-Z ]+$/', '/^[0-9-]+$/' );

      Die Prüfung kann nun so aussehen:

      $checkedvar = "default"; if (isset($_GET["inputvar"])) { foreach ($val as $k => $v) { if (preg_match($v, $_GET["inputvar"])) { $checkedvar = $_GET["inputvar"]; break; } } }

      Wenn $_GET["inputvar"] einen Wert hat, der auf eines der Muster in $val passt, wird dieser Wert in $checkedvar übernommen. Dazu müssen die Muster in $val natürlich vorne und hinten verankert sein (also mit ^ beginnen und $ enden), sonst kann man der Prüfung etwas unterschmuggeln. Paßt kein Muster, hat $checkedvar hinterher den Wert "default". Das Programm arbeitet dann ausschliesslich mit $checkedvar. Auf diese Weise können dem Programm keine beliebigen Werte aus $_GET["selectvar"] untergeschoben werden.

    3. Variablen, die eine numerische Form haben müssen.

      Numerische Variablen können durch eine einfache Konvertierung erzwungen werden. Danach muss eine Überpruefung des Wertebereiches erfolgen:

      $checkedvar = isset($_GET["numericvar"]) ? $_GET["numericvar"]+0 : 0;

      Durch die Addition von 0 wird eine Konvertierung auf Integer erzwungen. Der Wert ist hinterher entweder Wert des numerischen Prefix von $_GET["numericvar"] oder 0. "123abc" wird also zu 123, "fasel" zu 0. Ein fehlender Wert wird durch das isset() ebenfalls zu 0.

    4. Variablen, die Dateinamennatur haben.

      Programme sollen oft Dateinamen, aber keine Pfadnamen annehmen. Eine Prüfung auf "/" im Dateinamen reicht oftmals als Sicherheitsprüfung aus. Zwingt man außerdem noch die Endung hinten dran, erschwert man die Ausnutzung von Sicherheitslücken zusätzlich, sollten dennoch welche vorhanden sein.

      $checkedvar = "default"; if (isset($_GET["filenamevar"]) && !preg_match('=/=', $_GET["filenamevar"])) { $checkedvar = $_GET["filenamevar"] . ".ext"; }
    5. Templatenamen

      Templatenamen sind kein Spezialfall von Dateinamen oder Namen mit Formvorschrift, sondern ein Spezialfall von vorgegebenen Werten aus einer Liste und sollten wie der erste Fall behandelt werden. Danke.

    6. Benutzerinput, der Tags enthalten kann

      Cross Site Scripting Attacks sind Attacken, in denen eine Benutzereingabe aus einem Formular in die eigene Site als HTML, CSS oder anderer Formattext übernommen wird. Auf diese Weise kann der Benutzer eigene Elemente in unsere Website einbinden (etwa Bilder, Formulare oder Javascript von seiner Site), die einem Opfer dann als legitime Bestandteile unserer Site erscheinen und mit den Rechten unserer Site beim Opfer ausgeführt werden.

      strip_tags() auf GRUNDSÄTZLICH ALLE Benutzereingaben ist eine ausgesprochen gute Idee. Das schließt gegebenenfalls den Referer und andere benutzerkontrollierte Variablen mit ein! So existiert zum Beispiel eine Attacke gegen webalizer, bei der man einer Site Referer unterschiebt, die HTML enthalten. In der webalizer-generierten Referer-Statistik der Site erscheint dann feindseliges HTML!

Frage: Was genau bewirkt safe_mode und ist das sicher?

Antwort von Kristian Köhntopp:

Die hier aufgeführte Liste basiert auf einer Analyse einer bestimmten, inzwischen veralteten Version des PHP-Sourcecodes. Die grundsätzlichen Informationen sind korrekt, aber die Liste der einzelnen Teileinschränkungen dürfte nicht mehr abschließend sein.

Es gibt eine Konfigurationsvariable safe_mode , die in der php.ini gesetzt werden kann. Weiterhin gibt es die Konfigurationsvariablen safe_mode_exec_dir und sql_safe_mode .

Wenn safe_mode aktiv ist, sind verschiedene PHP-Funktionen privilegiert oder eingeschränkt. Zumeist gilt die safe_mode Einschränkung , dass auf eine Datei oder ein Verzeichnis nur eingewirkt werden darf, wenn die Datei oder das Verzeichnis denselben Eigentümer hat wie das Script. Im Einzelnen:

    • Alle Dateifunktionen einschließlich include() und require() können nur noch mit lokalen Dateien arbeiten, die denselben Eigentümer (uid) haben wie der Eigentümer des Scriptes.

    • Auch ftp-Pfadnamen werden so geprüft.

    • Auch zlib-Pfadnamen werden so geprüft.

    • mkdir() und posix_mkfifo() sind nur in Verzeichnissen zugelassen, die die Einschränkung erfüllen.

    • Das Ziel eines link() und symlink() (Parameter 1 von symlink() ) muss die Einschränkung erfüllen.

    • Die Subjekte der Funktionen unlink() , chgrp() , chown() , chmod() , touch() , rmdir() , rename() und copy() müssen die Einschränkung erfüllen.

Wenn sql_safe_mode aktiv ist, können bei einem MySQL-Connect host , user und password nicht angegeben werden ("SQL safe mode in effect - ignoring host/user/password information").

safe_mode ist nicht sicher: Ein Fehler in der popen() -Funktion ist erst mit 3.0.14 korrigiert worden, ein weiterer Fehler in der mail() -Funktion erst in 3.0.15. Spätere Versionen von PHP hatten weitere Lücken. Man sollte stattdessen die CGI-Version in einem chroot -Environment verwenden und mit setrlimit noch weitergehende Einschränkungen definieren.

Frage: Warum ist es schlecht, mit dem Referer zu arbeiten?

Antwort von Martin Jansen:

Der Referer ist eine Variable, in der stehen soll, von welcher Seite der Benutzer kommt, der sich gerade auf der Seite befindet. Hat der User zum Beispiel bei einer Suchmaschine auf einen Link geklickt, so würde der Referer "http://suchmaschine.tld/query?suchwort=german-faq" lauten.

Dies muss allerdings nicht immer so sein:

    • Die Entwickler des Browsers haben nicht vorgesehen, dass ein Referer-String mitgesendet wird oder der User hat das Mitsenden des Referers im Browser deaktivert.

    • Der User verwendet einen lokalen Proxyserver (z.B. Webwasher ), der die Referer-Information herausfiltert.

    • Ein Proxyserver bei einem Provider, im Rechenzentrum einer Universität oder in einem Unternehmen ist so konfiguriert, dass er keine Referer-Strings mitsendet.

    • Neben dem kompletten Entfernen des Referer-Strings aus den Headerdaten kann es auch möglich sein, dass der Referer durch o.g. Quellen modifiziert wird und dadurch unbrauchbar wird.

Diese Punkte sind Argumente dafür, den Referer nicht zu sicherheitsrelevanten Zwecken auf einer Website einzusetzen.

Frage: Für welche Zwecke ist der Referer zu gebrauchen?

Antwort von Johannes Frömter:

Für Webserver-Statistiken zum Beispiel. Im Standard-Format des access.log von Apache wird der HTTP-Referer erfasst (wenn der Browser einen liefert); bereiten Statistikprogramme diese Logfiles auf, können sie Aussagen über Klickpfade machen, z.B.:

    • Zugriffe: "Von welcher meiner Seiten wird am Häufigsten auf Seite xy gegangen?"

    • Einstiegsseite: "Auf welche Seiten kommen Besucher von außen häufig?"

    • Linkseiten: "Welche andere Seiten linken mich?"

    • Fehlerhafte Links: "Von welcher Seite kam der Besucher zu dem toten Link?"

Will man den Referer trotz der in http-referer beschriebenen Gegenargumente für weitergehende Zwecke benutzen (z.B. zum Schutz eines Programmdownloades vor direkter Verlinkung), sollte man unbedingt so vorgehen, dass Requests mit fehlendem Referer nicht behindert werden. Der Besucher kann völlig schuldlos an einem fehlenden Referer sein, z.B. wenn er hinter einem Proxy in der Firma sitzt. Daher darf man den Zugriff nur dann abblocken, wenn der Referer vorhanden ist, aber einen nicht erlaubten Wert hat. Bei der umgekehrten Vorgehensweise (Zugriff nur bei Referer von der eigenen Seite) verscherzt man es sich unweigerlich mit einem Teil seiner Besucher.

In PHP hat man über die Variable $_SERVER["HTTP_REFERER"] Zugriff auf den Referer. Die Funktion parse_url() dürfte beim Zerlegen des Strings sehr hilfreich sein, damit man ihn einfacher vergleichen kann.

Man sollte sich aber immer bewusst sein, dass der Referer keine 100%-ige Sache ist. Will (oder muss) man den Zugriff auf bestimmte Ressourcen "wasserdicht" gestalten, führt kein Weg an einer Session-Lösung vorbei.

Frage: Kann ich PHP-Dateien kompilieren und so vor Dritten schützen?

Antwort von Johannes Frömter:

Anders als beispielsweise bei Java wird PHP-Code erst zur Laufzeit (wenn das Script aufgerufen wird) kompiliert, d.h. der Programmcode liegt im "Klartext" in den .php -Dateien. Für normale Besucher einer von PHP erzeugten Webseite ist der Programmcode nicht einzusehen. Solange man die Scripte nur auf dem eigenen Webserver einsetzt, ist der Code für andere unsichtbar (siehe hierzu auch den Artikel ' php-code '). Gibt man PHP-Dateien jedoch (an Kunden) weiter, erhalten diese damit den Source Code. Wenn der Code nicht "Open Source" ist, sollte man per Vertrag das Copyright und die Nutzungsbedingungen genau regeln.

Von Zend gibt es als kommerzielles Produkt den Zend Encoder Unlimited , der PHP-Code in ein binäres Format überführt und damit weitgehend vor Ausspähung, Veränderung und Re-engineering schützt. Das Produkt ist nicht gerade billig, außerdem braucht man natürlich auf jedem Webserver, der kompilierten Code ausführen soll, ein Zusatzmodul (dieses allerdings ist kostenlos).

Nach dem gleichen Prinzip funktioniert der PHP Bytecode Compiler , der im derzeitigen Beta-Stadium allerdings noch keinen objektorientierten Code verarbeiten kann. Die Kompilierung erfolgt außerdem "online", der Encoder ist (noch) nicht zum Download verfügbar.

Auch microCODE macht aus PHP-Code einen nicht lesbaren Bytecode, der durch ein einzubindendes .so-Modul vom PHP-Interpreter ausgeführt werden kann.

Einen etwas anderen Weg gehen die Tools Code Obfuscator und POBS - sie kommen ohne serverseitige Module aus, da sie den Code nur für Menschen, nicht aber für den PHP-Interpreter "unleserlich" machen. Der Code Obfuscator ersetzt die (meist sprechenden) Variablennamen durch (nichtssagende) Konstrukte, so dass es schwer ist, die Funktion eines so bearbeiteten Scripts nachzuverfolgen. POBS macht dasselbe, geht aber noch weiter, indem auch Konstanten und Funktionsnamen verändert und Kommentare, Einrückungen sowie Leerzeilen entfernt werden. Der resultierende Code ist immer noch gültiger PHP-Code, aber seine Funktionsweise ist kaum mehr zu entziffern.

Frage: Apache: Wie kann ich ein Verzeichnis mit einem Passwort schützen?

Antwort von Kristian Köhntopp:

Die Konfiguration des Webservers ist auch noch einmal ausführlich im Handbuch zum Apache Webserver beschrieben. Philipp Imhof hat auf seinem Webserver ein Script installiert, dass eine an den jeweiligen Beispielfall angepasste Anleitung erstellt.

Im einfachsten Fall verwendet man HTTP Basic Authentication. Dazu ist mit dem Apache-Programm htpasswd eine Passwortdatei anzulegen, die die Benutzernamen und Passworte enthält. Optional kann man auch eine Gruppendatei anlegen, die pro Zeile den Namen einer Benutzergruppe und, durch einen Doppelpunkt getrennt, die Benutzer in dieser Gruppe enthält.

kris@valiant:~/www/kris.koehntopp.de > htpasswd -c etc/htpasswd kris
New password: test
Re-type new password: test
Adding password for user kris
kris@valiant:~/www/kris.koehntopp.de > htpasswd etc/htpasswd marit
New password: test
Re-type new password: test
Adding password for user marit
kris@valiant:~/www/kris.koehntopp.de > echo "users: kris marit" >>
etc/htgroup 

Die Option -c steht dabei für create ; die Passwortdatei wird neu angelegt. Ohne die Option werden Benutzer zu einer existierenden Passwortdatei zugefügt bzw. die Passworte existierender Benutzer werden geändert. Im Beispiel wird weiterhin eine Gruppe users angelegt, der die Benutzer kris und marit angehören.

Ein Verzeichnis kann man nun durch das Anlegen einer .htaccess -Datei schützen (dazu muss AllowOverride AuthConfig in der httpd.conf für dieses Verzeichnis gesetzt sein) oder indem man die entsprechenden Konfigurationsanweisungen direkt in einen <Directory> -Block für dieses Verzeichnis in die httpd.conf einsetzt. Im einfachsten Fall sind dies die Anweisungen:

AuthType Basic
AuthName MyRealm
AuthUserFile /home/www/servers/www.koehntopp.de/etc/htpasswd
AuthGroupFile /home/www/servers/www.koehntopp.de/etc/htgroup

require valid-user 

Die Anweisung AuthType legt fest, auf welche Weise der Browser das Passwort übermittelt - bei der sehr unsicheren Basic-Authentication wird es nahezu im Klartext übertragen. Für den zu schützenden Bereich muss mit AuthName ein Name festgelegt werden. Die Direktiven AuthUserFile/ und AuthGroupFile legen die Datenquellen für die Authentisierung fest.

Damit ein Anwender den geschützten Bereich betreten kann, muss er die in der require -Anweisung geforderten Bedingungen erfüllen. Im einfachsten Fall muss er einfach nur in der htpasswd -Datei stehen und das passende Passwort wissen. Dies ist der Fall bei require valid-user . Mit komplizierteren Anweisungen kann man den Zutritt auch noch auf bestimmte Gruppen ( require group users ) oder auf bestimmte Benutzer ( require user kris ) einschränken.

In CGI PHP hat man in einem auf diese Weise geschützten Bereich Zugriff auf den Benutzernamen in der Variablen $_SERVER["REMOTE_USER"] und die verwendete Authentisierungsmethode in $_SERVER["AUTH_TYPE"] .

<?php
echo "AUTH_TYPE = ", $_SERVER['AUTH_TYPE'], "<br />\n";
echo "REMOTE_USER = ", $_SERVER['REMOTE_USER'], "<br />\n";
?> 

Im Apache-Modul hat man in einem geschützten Bereich außerdem Zugriff auf die vollen Authentisierungsdaten.

<?php
echo "AUTH_TYPE = ", $_SERVER['AUTH_TYPE'], "<br />\n";
echo "REMOTE_USER = ", $_SERVER['$REMOTE_USER'], "<br />\n";
$a = getallheaders();
$au = split(" ", $a["Authorization"], 2);
list($u, $p) = split(":", base64_decode($au[1]));
echo "Decodiert zu User $u, Password $p<br />\n"; 

Frage: Apache: Wie kann ich ein Verzeichnis mit PHP mit einem Passwort schützen?

Antwort von Kristian Köhntopp:

Dies ist in englischer Sprache ausführlich im PHP Handbuch beschrieben. Das Feature steht nur dann zur Verfügung, wenn PHP als Apache-Modul betrieben wird.

<?php
function check_pw($u, $p) {
  ...
}

if (!isset($_SERVER['PHP_AUTH_USER']) 
 or !check_pw($_SERVER['PHP_AUTH_USER'],$_SERVER['PHP_AUTH_PW']))
{
    Header("WWW-Authenticate: Basic realm=\"My Realm\"");
    Header("HTTP/1.0 401 Unauthorized");
    echo "Text to send if user hits Cancel button\n";
    exit;
} else {
    echo "Hello $PHP_AUTH_USER.<P>";
    echo "You entered $PHP_AUTH_PW as your password.<P>";
}
?> 

Nur wenn PHP selbst die Authentisierung vornimmt, stehen die Variablen PHP_AUTH_USER und PHP_AUTH_PW zur Verfügung. Sie können verwendet werden, um den Benutzer in einer Datei, einer Datenbank oder einer anderen Datenquelle nachzuschlagen und das Passwort zu überprüfen.

Frage: Kann ich mit CGI PHP ein Verzeichnis mit einem Passwort schützen?

Antwort von Kristian Köhntopp:

Nicht mit den Bordmitteln von PHP. Zwar kann man durch Senden eines Status -Header die Authentisierung auslösen, aber PHP übermittelt nicht die notwendigen Variablen an das Script zurück, wenn der Benutzer sich angemeldet hat.

Stattdessen sollte man Sessions verwenden und sich selbst ein Login basteln.

Frage: Wie kann ich Passwörter sicher speichern?

Antwort von Alex Kiesel:

Um Passwörter nicht im Klartext in eine Datenbank speichern zu müssen, gibt es die Möglichkeit diese vorher mit einer Einweg-Verschlüsselung zu versehen, bzw. nur einen Hash des Passworts abzuspeichern. Die Funktionen crypt() und md5() können hierfür benutzt werden.

Crypt verschlüsselt auf den meisten Systemen das Passwort mit DES, es werden jedoch nur die ersten 8 Zeichen zur Verschlüsselung benutzt, längere Passwörter werden also einfach abgeschnitten.

MD5 ist eine kryptographische Hash-Funktion. Sie berechnet für einen beliebigen Eingabestring einen Hash, der 128 Bit lang ist.

Beide Funktionen haben Eigenschaften, die uns hier sehr nützlich sind:

    • Sie sind determiniert: sie geben für eine bestimmte Eingabe immer das gleiche Ergebnis zurück.

    • Es lässt sich nicht vom Ergebnis auf die Eingabe schliessen. Damit ist eine "Rückrechnung" des Passwort-Hashes auf das echte Passwort des Benutzers nicht möglich (außer durch Brute-Force).

    • Es ist sehr unwahrscheinlich (wenn auch nicht unmöglich), dass zwei verschiedene Eingaben genau denselben Hash-Wert ergeben.

Um die Passwörter der Benutzer also sicher zu speichern, wird vor dem Speichern in z.B. einer Datenbank erst der Hash des Passworts berechnet und dieser dann gespeichert. Da wir (und vor allem andere) nicht in der Lage sind, aus dem gespeicherten String das wirkliche Passwort zu berechnen, kann bei der Prüfung der Passworteingabe nicht das Passwort direkt verglichen werden. Wir berechnen zunächst also wieder den Hash der Eingabe und vergleichen diesen mit dem gespeicherten Hash. Wegen der o.g. Eigenschaft wird der Hash der Eingabe (i.A.) genau dann gleich dem gespeicherten Hashwert sein, wenn die ursprünglich eingegebenen Passwörter gleich sind.

Auch wenn die Passwörter nun sicher in der Datenbank gespeichert sind, sollte hier nicht übersehen werden, dass die Eingaben des Benutzers, also auch das Passwort, bei einer normalen HTTP-Übertragung unverschlüsselt über die Leitung gehen und somit nicht sicher sind. Eine mögliche clientseitige Verschlüsselung z.B. durch Javascript, bei der nur das Passwort verschlüsselt wird, ist keine Abhilfe dagegen. Hier würde zwar das Passwort nicht ersichtlich sein, zum Anmelden würde es aber reichen, den mitgesnifften Hash an den Server zu schicken. Letztlich bleibt für eine sichere Authentifizierung nur die Übertragung über HTTPS.

Frage: Vermeide globale Variablen

Antwort von Kristian Köhntopp:

In veralteten Versionen von PHP wurden eine ganze Menge Daten aus der Prozessumgebung und aus dem Internet in den globalen Namensraum importiert. Das war sehr bequem, aber auch ein Quell von zahlreichen Exploits in vielen weit verbreiteten PHP-Scripten. Mit der Version 4.2 wurde das Standardverhalten von PHP geändert: Diese Informationen stehen jetzt nur noch in besonderen, superglobalen Arrays zur Verfügung (siehe php-variablen und formular-register-globals ).

Versionen von PHP 4.2 und höher zeigen ebenfalls dieses obsolete Verhalten, sobald die Konfigurationsvariable register_globals wieder auf den Wert On gestellt wird. Es ist dringend empfohlen, diese Variable auf Off zu lassen.

Daher war es in diesen veralteten Versionen von PHP dringend notwendig, globale Variablen möglichst zu vermeiden, um Sicherheitsproblemen aus dem Weg zu gehen.

Dieser Abschnitt enthielt eine Reihe von Informationen darüber, nach welchen Regeln PHP den globalen Namensraum mit externen Daten vermatschte. Diese Informationen sind nun glücklicherweise obsolet, und wurden daher gestrichen.

Die Empfehlung, möglichst keine globalen Variablen zu verwenden bleibt jedoch bestehen: Der Namensraum von Funktionen und Klassen ist wesentlich besser kontrollierbar. Es ist daher empfehlenswert, so viel Funktionalität als möglich in Funktionen oder Klassen abzulegen und dort kontrolliert Variablen als Funktionsparameter zu importieren.

Frage: Prüfe importierte Parameter. Traue niemandem

Antwort von Kristian Köhntopp:

Parameter, die als GET, POST oder COOKIE-Werte in das lokale Programm gekommen sind, sind nicht vertrauenswürdig. Techniken, die Variablen als GET-Parameter am Ende einer URL durchschleifen oder als HIDDEN-Variablen in Formularen weitergeben ( Ping-Pong-Technik ), sind daher prinzipbedingt fehlerhaft und können niemals sicher sein.

Auch dürfen Variablen aus GPC-Quellen niemals ungeprüft direkt verwendet werden, sondern müssen zwingend erst einmal auf Ungefährlichkeit geprüft werden (siehe security-variablen ). Bei Parametern, die gegen die Empfehlung per Ping-Pong weitergereicht werden, muss dieser Test jedesmal (auf jeder Seite) wieder gemacht werden, da ja ein Anwender oder ein böswilliges Programm die Werte inzwischen geändert haben könnte.

Eine Webanwendung kontrolliert nur den Bereich bis zur Firewall. Der Browser des Anwenders befindet sich auf der anderen Seite der Firewall und ist daher als Feindesland anzusehen. Daten, die von dort kommen, können unerwartete Werte oder unerwartete Formate haben. Insbesondere kann eine Webanwendung keine Annahmen über bestimmte Eigenschaften des Browsers machen, wie zum Beispiel "JavaScript wird zuverlässig ausgeführt", "Cookies werden akzeptiert und verändern ihre Werte nicht spontan" oder "Referer-Header sind vertrauenswürdig".

Es gelten daher die folgenden Empfehlungen:

    • Jede Form von Ping-Pong, also Wert-Weitergabe durch GET-Parameter, HIDDEN-Variablen und dergleichen ist zu vermeiden. Auch durch Codierung der Werte ist hier nichts zu erreichen.

      Stattdessen sind Sessionvariablen zu verwenden. Nur die Session-ID wird von einer Seite an eine andere Seite weitergereicht.

    • Keinesfalls darf ein Programm Werte aus einer GET, POST oder COOKIE-Quelle direkt verwenden. Jeder externe Wert ist einer Plausibilitätsprüfung zu unterziehen, bevor er verwendet wird (Genau das wird in security-variablen näher beschrieben).

    • Validierung von Eingabewerten muss serverseitig durch PHP geschehen. JavaScript-Validatoren sind nicht vertrauenswürdig: Der Browser des Anwenders führt diese Validatoren möglicherweise nicht aus, auch dann, wenn er sich durch die User-Agent-Zeile als JavaScript-fähiger Browser identifiziert. Ebenso muss angenommen werden, dass die Referer-Header des Browsers möglicherweise gefälscht sind. Man kann nicht annehmen, dass ein Zugriff tatsächlich von einer bestimmten vorhergehenden Seite hierher vermittelt wurde.

Zusammenfassend: Traue niemandem. Validiere allen Input oder stirb.

Frage: Was sind Race Conditions? Wie kann ich sie vermeiden?

Antwort von Frank Wiegand:

Wenn das Ergebnis mehrerer Ereignisse von deren Reihenfolge abhängt, und diese Reihenfolge zeitlich nicht garantiert werden kann, dann spricht man von einer Race Condition . Der Name "Race Condition" leitet sich davon ab, dass mehrere Prozesse eine Art Rennen um die betroffene Ressource austragen, welches nur einer gewinnen kann.

Ein typisches Beispiel für eine Race Condition:

<?php

  $datei = '/home/frank/datei';

  // wenn $datei nicht existiert ...
  if (!file_exists ($datei)) {
    // ... dann $datei zum Schreiben öffnen
    $fp = fopen ($datei, 'w');

    // andere Zugriffe abblocken
    flock ($fp, LOCK_EX);
    
    // Daten in die Datei schreiben
    fputs ($fp, $data);

    // Datei freigeben
    flock ($fp, LOCK_UN);
  }
  else {
    // Datei existiert bereits
  }
  
?> 

Nachdem der Test mit file_exists() von dem Script (Prozess 1) durchgeführt worden ist, kann es sein, dass der Prozessor zunächst einem anderen Prozess (Prozess 2) Rechenzeit gewährt. Der Vorgang Test auf Existenz und Lockfile anlegen läuft nicht atomar , sondern geteilt ab. Prozess 2 kann in der Zwischenzeit die Datei $datei als symbolischen Link auf eine andere Datei anlegen. Das ist besonders gefährlich, da Prozess 2 beispielsweise nicht unbedingt Schreibrechte an der gelinkten Datei haben muss. Wenn Prozess 1 wieder an der Reihe ist, wird die von Prozess 2 verlinkte Datei zum Schreiben geöffnet. Ein Eindringling kann dies ausnutzen und mithilfe von Prozess 1 wichtige Daten überschreiben lassen. Oft hört man das Argument, dass dieser Fall äußerst unwahrscheinlich ist. Das stimmt nicht - oft reicht es schon aus, die entsprechenden Programme häufig (mehrere Male pro Sekunde) auszuführen. Es ist nur eine Frage der Zeit, wann die Race Condition auftritt.

Die Race Condition wird vermieden, indem man die Operation des Lockfile anlegen atomar , d. h. unteilbar ausführt. Dazu werden die Daten zunächst in eine temporäre Datei geschrieben. In PHP gibt es die Funktion tempnam() , welche eine temporäre Datei mit eindeutigem Namen anlegt. Anschliessend erstellt man die gewünschte mit der Funktion link() an der entsprechenden Stelle. link() ist eine atomare Funktion und gibt false zurück, wenn die Datei nicht erstellt werden konnte. Dabei ist zu beachten, dass link() nicht über Dateisysteme hinweg funktioniert. Die temporäre Datei muss also im selben Dateisystem (auf derselben Partition) angelegt werden.

Hier nun das Beispiel ohne Race Condition:

<?php
  
  $datei = '/home/frank/datei';
  
  // temporäre Datei anlegen
  $tempnam = tempnam ('/home/frank/tmp', 'foo');

  // Fehler: Datei konnte nicht angelegt werden
  if ($tempnam === false)
    die ("Could not create $tempnam!");
  
  // temporäre Datei zum Schreiben öffnen
  $fp = fopen ($tempnam, 'w') or die ("Could not open $tempnam!");

  // andere Zugriffe abblocken
  flock ($fp, LOCK_EX);

  // Daten in die Datei schreiben
  fputs ($fp, $data);

  // Datei freigeben
  flock ($fp, LOCK_UN);
  fclose ($fp);

  // $datei zu temporärer Datei hardlinken:
  // schlägt fehl, wenn $datei schon vorhanden ist
  if (!link ($tempnam, $datei)) {
    // Fehlschlag: Datei bereits vorhanden
  }

  // temporäre Datei löschen
  unlink ($tempnam);

?> 

Unter Windows existiert die Funktion link() nicht. Stattdessen kann man rename() verwenden. Mit der Verwendung von rename() entfällt auch das Löschen der temporären Datei. rename() unter Windows überschreibt nicht vorhandene Dateien, während rename() unter UNIX/Linux dies tut.

Allgemein gilt also: Atomare Operationen wie z. B. Operationen auf Dateien müssen atomar (d. h. in einem Schritt) ausgeführt werden. Viele Funktionen geben einen booleschen Wert zurück, mit dem man überprüfen kann, ob die Operation erfolgreich war.

Diese Funktionen sind ein Anzeichen dafür, dass man eventuell eine Race Condition programmiert hat: