Warnung: Merkwürdige Serverkonfiguration bei Strato (u.a.) kann zu Sicherheitsproblemen führen

Strato (und womöglich auch andere Hoster!) konfiguriert bei den Angeboten für Privat- und Firmenkunden ("shared hosting") die "virtuellen Hosts" auf eine eher nicht erwartete und, wie sich zeigen wird, auf ungünstige Art.

Legt man in das Wurzelverzeichnis eines Webauftritts eine PHP-Datei mit folgenden Inhalt,

header('Content-type: text/plain');
echo '__DIR__                   : ', __DIR__ , "\n";
echo '$_SERVER["DOCUMENT_ROOT"] : ', $_SERVER["DOCUMENT_ROOT"] , "\n";

dann erhält man beim einem Abruf über die URL "http://[Domain_bei_Strato]/testskript.php" unerwartete Ergebnisse - wenn man, wie die Strato-Kunden, betroffen ist. Dann sehen die Ergebnisse wie unter 3. gezeigt aus:

1.) Zuerst der Normalfall, wie man ihn z.B. auf Debian Systemen nach dem Setup des Apache vorfindet:

__DIR__                   : /var/www/htdocs
$_SERVER["DOCUMENT_ROOT"] : /var/www/htdocs

2.) Bei der Mehrzahl der Hoster, welche "shared hosting" anbieten dann etwa so: (immer noch "Normalfall")

__DIR__                   : /var/kunden/0814/web_A
$_SERVER["DOCUMENT_ROOT"] : /var/kunden/0815/web_A

(Hier die Ausgaben konkret für diesen Host)

3.) Nicht  jedoch bei Strato, dort sieht das, zur Verdeutlichung grob verkürzt dargestellt, so ähnlich aus:

__DIR__                   : /FOO/kunde/web_A
$_SERVER["DOCUMENT_ROOT"] : /BAR/kunde/web_A

Strato hat also Links im Dateisystem gesetzt und diese dann benutzt um die virtuellen Hosts zu konfigurieren. Das ist aus technischer Sicht nicht die "sauberste" Lösung.

Das wird von vielen Entwicklern nicht erwartet und kann unter Umständen zu einem Sicherheitsproblem führen. Wenn eine Anwendung programmiert wird, welche Funktionen oder Objekte mittels require, require_once, include oder include_once aus Bibliotheks-Dateien integriert, dann kann es in vielen Fällen vorkommen, dass diese Dateien nicht direkt abrufbar sein sollen. Dieses häufig deshalb, weil nur das aufrufende Skript eine Authentifizierung prüft. Eine gängige, allerdings eher "viertbeste" Lösung hierfür ist, diese Bibliotheken außerhalb des Document-Root zu abzulegen und, zur "Sicherheit", einfach und bequem abzufragen, ob das auch wirklich so der Fall ist.

Und wie sollte das einfach und "billig" (schnell) aber auch "schmutzig" gehen? Ganz einfach. Man prüft ob der Pfad, den $_SERVER["DOCUMENT_ROOT"] als String zurück gibt, in dem enthalten ist, den __DIR__ liefert...

## file 'only_included.php'

# Exit, wenn in document_root, weil dann Sicherheitsproblem auftreten kann:
if ( 0 == strpos(__DIR__, $_SERVER["DOCUMENT_ROOT"]) ) {
    trigger_error('Fatal: '. __FILE__
         . ' darf nicht in oder unterhalb von DOCUMENT_ROOT ('
         . $_SERVER["DOCUMENT_ROOT"] . ') liegen!', E_USER_ERROR);
    exit;
}

## weiterer Skriptablauf, z.B. das Speichern übertragener Daten als Datei.

Das Problem ist, dass bei Webauftritten, welche auf shared hosts von Strato liegen (andere können auch betroffen sein) __DIR__ und  $_SERVER['DOCUMENT_ROOT'] auch in den Fällen verschiedene Pfade zurück liefern, in welchen aber der gleiche erwartet wird. Das bedeutet: Dieses Skript wird fälschlicherweise stets annnehmen, es befinde sich außerhalb von $_SERVER['DOCUMENT_ROOT'], sei also nicht direkt aufrufbar und deswegen "sicher" - was aber nicht zutrifft.

Workarround

Einen möglichen Workarround schlägt Strato selbst vor. Nämlich die Benutzung von realpath(), eine PHP-Funktion, welche einen übergebenen Pfad überprüft und in diesem vorkommende "symbolische" Links durch den korrekten Pfad des Dateisystems ersetzt. Zum Test:

header('Content-type: text/plain');
echo '__DIR__                    : ', __DIR__ , "\n";
echo 'realpath($_SERVER["DOCUMENT_ROOT"]): ', realpath($_SERVER["DOCUMENT_ROOT"]), "\n";

Dann stimmts auch bei Strato. Das obige Skript wäre dann - als "Workarround" so zu verändern:

## file 'only_included.php'

# Exit, wenn in document_root, weil dann Sicherheitsproblem auftreten kann:
if ( 0 == strpos(__DIR__, realpath($_SERVER["DOCUMENT_ROOT"])) ) {
    trigger_error('Fatal: '. __FILE__
         . ' darf nicht in oder unterhalb von DOCUMENT_ROOT ('
         . realpath($_SERVER["DOCUMENT_ROOT"]) . ') liegen!', E_USER_ERROR);
    exit;
}

## weiterer Skriptablauf, z.B. das Speichern übertragener Daten als Datei.

Problem mit dem Workarround

Das Problem sind und bleiben "Fertigskripte" aus dem Web - und hier ist zu unterstellen, dass gerade auch viele Kunden  der Strato AG solche benutzen ohne diese zu verstehen. Selbst "gestandene" PHP-Programmierer haben das Problem nicht auf Anhieb erkannt - weshalb ich davon ausgehe, dass dieser Fehler und das womöglich resultierende Sicherheitsproblem in einer Vielzahl von Fällen nicht erkannt wurde und wird.

Der von mir kontaktierte Strato-Kundendienst hat mir bedeutet:

  1. Strato bestätigt, dass ich mit meiner Feststellung bezüglich der Merkwürdigkeit "völlig Recht" habe.
  2. Strato verwendet diese merkwürdige Konfiguration seit 12 Jahren
  3. Diese Konfiguration sei vom Unternehmen gewünscht.
  4. "Wir empfeheln daher die Verwendung des realpath($_SERVER[\'DOCUMENT_ROOT\']), so wie dies auch von jeder Webanwendung gehandled wird."

Nach der "sicherlich nicht in jedem Fall richtigen" Auskunft zu den Webanwendungen bedankt sich Strato noch für mein Verständnis. Welches ich aber gar nicht aufbringe und auch keine Notwendigkeit für die von mir als unnötig und sogar gefährlich gehaltene Konfiguration sehe. In der Dokumenation des Webservers steht ausdrücklich:

Seien Sie vorsichtig mit den Verzeichnispfad-Argumenten. Sie müssen buchstäblich mit dem Dateisystempfad übereinstimmen, den der Apache für den Zugriff auf die Dateien verwendet. Direktiven, die für ein bestimmtes Verzeichnis gelten, gelten nicht für Dateien in dem Verzeichnis, auf die über einen anderen Pfad zugegriffen wird, wie z.B. über verschiedene symbolische Links.

Zudem muss hier auch die Option "Follow Symlinks" für den Server explizit eingeschaltet worden sein. Offenbar halten das die Programmierer des Webservers Apache nicht für eine so gute Idee, dass diese das als Default setzen. Zudem kostet die Verwendung von realpath() auch Rechenzeit und ist damit im Programmierer-Jargon "teuer".

Bester Workarround

Die Lösung der Überprüfung des Dateisytem-Pfades ist viertklassig, man kann dazu auch "zweifelhaft" sagen. Wenn eine Berechtigung geprüft wird, dann wird in aller Regel auch ein Authentifizierungsmerkmal zur Verfügung stehen, z.B. in $_SESSION. Das zu prüfen ist nicht schwieriger. Dazu kommt, dass in Includes regelmäßig Funktionen oder Objekte (Klassen) stehen sollten. Haben die Includes kein "Hauptprogramm", dann gibt es auch kein Problem. Insofern liegt es nicht nur an Strato, sondern "nur" auch.


Weitere Informationen in diesem Bereich: