X-Sendfile zur Sicherung von Userinhalten

Für viele unserer Kunden ist die Datensicherheit ihr höchstes Gut: Dabei geht es aber nicht nur um den Schutz ihrer Daten vor fremdem Zugriff von außen, sondern auch um die Sicherheit der Daten einzelner User untereinander. Seien es hochgeladene Texte, Profilbilder oder gar Lebensläufe, es muss garantiert werden, dass nur berechtigte User diese Daten sehen dürfen.

Leider ist dies bei vielen Webseiten nicht der Fall. Zumeist liegen alle hochgeladenen Dateien öffentlich zugänglich in einem Unterordner des htdocs-Verzeichnisses und der einzige Schutz, der vor einem unerlaubten Zugriff existiert ist die Tatsache, dass die Dateinamen dem Aufrufer unbekannt sind.
Dies ist natürlich nur ein marginaler Schutz, insbesondere weil man sich aus eigenem Interesse bemühen sollte, die Dateien sinnvoll und ordentlich zu benennen.

Die einfachste Lösungsmöglichkeit für dieses Problem ist es nun, die Dateien außerhalb des htdocs-Verzeichnisses zu speichern und dann mit Hilfe einer Skriptsprache wie PHP als Stream auszuliefern. Das folgende Skript demonstriert dies beispielhaft:

<?php
    //Achtung, im Produktiveinsatz sollte hier noch überprüft werden ob der Benutzer berechtigt ist
    //die Datei abzurufen
    if(!empty($_GET['file'])){
        //$config ist z.B. ein Objekt welches die Konfiguration der Seite gespeichert hält
        $file = $config->getFilePath().$_GET['file'];
        if(file_exists($file)){
            header ('Content-Type: application/octet-stream');
            header ('Content-Length: '.filesize($file));
            readfile($file);
        }
     }
?>

Diese Methode hat jedoch drei große Nachteile:

  1. Die Methode wirkt sich negativ auf die Performance aus, da die Datei nicht direkt vom Webserver übertragen wird, sondern ein Prozess der Skriptsprache solange läuft, bis die Datei komplett gestreamt wurde. Gerade bei hochfrequentierten Seiten kann die Ausführung dieser vielen einzelnen Skript-Prozesse die Performance der Seite sehr belasten, da die Prozesse von solchen Skripten einen unnötigen Speicherbedarf erzeugen.
  2. Viele Webserver beschränken (sinnvollerweise) die Ausführungszeit der Aufrufe von Skripten (z.B. auf 60 Sekunden). Bei größeren Dateien würde dies unweigerlich zum Abbruch des Downloads führen.
  3. Im Normalfall werden vom Webserver Skriptaufrufe nicht gecacht, da es sich ja um dynamische Inhalte handelt. Bei der Auslieferung von Dateien wollen wir aber natürlich den Vorteil des Cachings nur ungern verlieren, wie z.B. über E-Tags oder conditional GETs.

Übertragung mit X-Sendfile – Die bessere Variante

Die Apache2 Mod X-Sendfile bietet eine elegantere Möglichkeit, das obige Konzept der Auslagerung der Dateien außerhalb des htdocs-Verzeichnisses umzusetzen. Die Idee dieser Mod ist einfach: Anstatt dem Server nur die Inhalte des fertig ausgelieferten Skripts zu übergeben, wird dem Skript ermöglicht mit dem Webserver zu „kommunizieren“. Das geschieht, indem Apache die Skriptdatei nach dem gesetzen X-Sendfile Header durchsucht. Sollte dieser gefunden werden, wird statt dem Skript die im X-Sendfile Header angegebene Datei übertragen. Das folgende Skript demonstriert die obige Dateiauslieferung, diesmal mit X-Sendfile umgesetzt:

<?php
    //Achtung, im Produktiveinsatz sollte hier noch überprüft werden ob der Benutzer berechtigt ist
    //die Datei abzurufen
    if(!empty($_GET['file'])){
        //$config ist z.B. ein Objekt welches die Konfiguration der Seite gespeichert hält
        $file = $config->getFilePath().$_GET['file'];
        if(file_exists($file)){
            header('X-Sendfile: '.$file);
            header ('Content-Type: application/octet-stream');
            header ('Content-Disposition: attachment; filename='.$file );
            exit;
        }
     }
?>

Zugangsschutz mit X-Sendfile

Nachdem wir nun eine Möglichkeit gefunden haben, unsere Dateien vor einem direkten Zugriff über den Dateinamen zu schützen, können wir unsere Dateien effektiv vor einem Fremdzugriff sichern. Als ein einfaches Beispiel nehmen wir an, dass unsere Webseite die Lebensläufe ihrer User ausliefern soll und zwar nur an den Besitzer des Lebenslaufs selbst oder an eine Firma bei der sich der User beworben hat. Wir haben die Lebensläufe schon außerhalb des htdocs-Ordners gespeichert und wollen diese nun über die Datei get_cv.php ausliefern.

Unser HTML Link würde folgendermaßen aussehen:

<html>
    ...
    <body>
        ...
        <p>
             <a href="get_cv.php?userid=10">Lebenslauf herunterladen</a>
        </p>
    </body>
</html>

Die PHP Datei selbst überprüft nun zunächst ob der Benutzer der Besitzer des Lebenslaufs ist, oder ob er sich bei der Firma, die den Link aufruft, beworben hat. Aus Gründen der Übersichtlichkeit gehen wir davon aus, dass einige Objekte und Methoden an anderer Stelle implementiert wurden.

<?php
    if(!empty($_GET['userid'])){
         //Der Usermanager verwaltet alle User unseres Portals
         $user = $usermanager->getUser(intval($_GET['userid']));
         $loggedInUser = $usermanager->getCurrentLoggedInUser();

         //Der Lebenslauf gehört uns selbst, wir dürfen ihn downloaden
         if($user->getId() == $loggedInUser->getId()){
            header('X-Sendfile: '.$loggedInUser->getCV());
            header ('Content-Type: application/octet-stream');
            header ('Content-Disposition: attachment; filename='.$loggedInUser->getCV() );          
         }
         //Der Besitzer des Lebenslaufs hat sich bei der Firma die im Moment eingeloggt ist beworben
         else if($loggedInUser->isCompany() && $loggedInUser->userAppliedForJob($user)){
            header('X-Sendfile: '.$loggedInUser->getCV());
            header ('Content-Type: application/octet-stream');
            header ('Content-Disposition: attachment; filename='.$loggedInUser->getCV() );
         }else{
              die("Es wurde keine User-ID übergeben.");
         }
    }else{
        die("Es wurde keine User-ID übergeben.");
    }
?>

Mit diesem einfachen Skript haben wir unsere Dateien vor unbefugtem Zugriff geschützt und können uns beliebig komplexe Zugriffsregeln ausdenken und in PHP umsetzen. Natürlich bieten sich einem auch noch viele weitere Möglichkeiten durch die Vorschaltung eines PHP Skripts, exemplarisch seien hier genannt:

  • Abrufen von Informationen aus der Datenbank, z.B. um den Dateiennamen anzupassen
  • Speichern von Daten beim Aufruf, z.B. ein Downloadzähler
  • Bezahlung der Inhalte vor dem Ausliefern der Dateien

Sicherheitsbedenken

Zum Abschluss möchte ich noch auf eine Gefahr hinweisen, die durch dein Einsatz von X-Sendfile enstehen kann. Da der Webserver jede Datei, die im X-Sendfile Header angegeben wird und auf die er lesend zugreifen darf, auch ausliefert, muss der Programmierer sehr genau aufpassen, dass er keine große Sicherheitslücke in seinem Server öffnet.

Dies ist insbesondere dann der Fall, wenn der Benutzer die gewünschte Datei z.B. über einen GET-Parameter an das PHP Skript übergibt. In diesem Fall kann der Angreifer über dein Einsatz des ..-Operators auch auf tiefere Verzeichnisse des Dateisystems zugreifen.

Es ist also zu empfehlen entweder dem Benutzer nicht direkt die Möglichkeit zu geben, den Dateinamen anzugeben, oder zumindest den übergebenen Dateinamen so zu bereinigen, dass kein Schaden enstehen kann. Dies ist zum Beispiel sehr einfach mit str_replace möglich:

<?php
    str_replace("..","",$filename);
?>

Wenn X-Sendfile jedoch richtig und sicher eingesetzt wird, erhöht es sowohl die Sicherheit wie auch Performance eines Webportals um ein Vielfaches.

In Technischer Hintergrund veröffentlicht | Getaggt: , , , ,