Htaccess RewriteRule - PDF-Dateien schützen [gelöst]

Hallo,

wieder mal ein PHP-Problem vom Anfänger…

Folgendes Szenario: ich habe einen Kunden-Login “gebaut” (mit Sessions). Für jeden Kunden existiert eine Seite, in der er seine Verträge (als PDF, liegen in einem eigenen Unterverzeichnis) anzeigen bzw. herunterladen kann.
Nun ist es aber möglich, durch Eingabe der kompletten URI darauf zuzugreifen - ohne sich vorher anzumelden.
Das will ich natürlich gerne verhindern… leider ist ein zusätzlicher Schutz mit .htaccess + .htpasswd nicht praktikabel (er müsste sich ein weiteres mal einloggen, und könnte das Passwort dafür auch nicht selber ändern).

Wie kann ich also verhindern, dass PDF-Dateien direkt aufrufbar sind?
RewriteRules scheinen da praktikabel zu sein - nur leider führt das:

RewriteEngine On RewriteRule ^(.*\.(pdf))$ /restricted/index.php [R]dazu, dass die PDF-Dateien gar nicht mehr angezeigt werden können (auch nicht bei vorherigem Login).
Ich brauche also eine Regel, die alle Aufrufe von ausserhalb des Userverzeichnisses (/restricted/user) auf die Login-Seite umleitet (/restricted/index.php), von innerhalb des Userverzeichnisses aber zulässt.

Machbar?
Oder Denkfehler?
Andere Lösungen?

keine gute Lösung wäre mit dem Refferrer möglich:

ReriteEngine On
RewriteCond %{HTTP_REFERER} !^http://(www\.)?XYZ.de/restricted$ [NC]
RewriteRule ^(.*\.(pdf))$ /restricted/index.php [R]

Besser ist diese Lösung:

RewriteEngine On
RewriteRule (^(.*\.pdf))$ /restricted/download.php?file=$1 [QSA]

und in der download.php:

<?php

session_start();

$filename = "../".$_GET['file']; //oder eine ähliche Pfadanpassung ;)

if($_SESSION['logged_in']) //oder eine ähnliche Anfrage
{
   header("content-type: application/pdf");
   header("content-lenght: ".filesize($file));
   readfile($file);
}
else
{
   echo "Sie haben keine Berechtigungen $file herunterzuladen ;)"

; } ?>

Michis zweite variante ist die bessere Möglichkeit. :3
Lieber php das auslesen aus dem Verzeichniss übernehmen lassen, und den Zugriff auf den Ordner via Webserver komplett per .htaccess verbieten (deny from all)

beim header würde ich noch hinzufügen:

Damit kein Browser auf die verrückte Idee kommt, die PDF im Browser anzeigen zu wollen, sondern die Datei schön als download anbietet (man kann dann auch den Dateinamen speziell anpassen).

mfg Balmung

Auf das hab ich absichtlich verzichtet, da der Adobe Reader es ja anbietet das Dokument im Browser anzuzeigen :wink:

Danke für die Tipps.

Trotzdem würde die Weiterleitung auf die “Download-Seite” (gibt es in der Form zwar nicht, aber das ist hier egal) so nicht funktionieren: ich arbeite mit Sessions.
Bisher sehen Links und Formulare so aus:

-- Dateianfang--
<?php   
  session_start();
  $sn = session_name();
  $sid = session_id();

  if ($_GET[$sn] != $sid or !isset($_SESSION['username'])) header("Location:login_error.php");
  else { echo "
... head, body, divs, das ganze HTML-Zeugs halt ...

    <a href='linkziel.php?'".$sn."=".$sid."'>Linktext</a>...
...
}
?>

Im oberen Teil erfolgt die Überprüfung auf vorhandenen Login (per Session-ID).
Im unteren Teil seht ihr, wie ich die Links anpasse (Session Name und -ID werden als Parameter angehängt).

Funktioniert prächtig, geht aber nicht mit .htaccess.
Dort würde ich immer auf login_error.php landen, weil keine Session ID geliefert wird / werden kann.

Außerdem will ich es vermeiden, Dateinamen mit GET zu übermitteln…
Und erschwerend kommt hinzu, dass ich die Dateinamen nicht kenne (Rechnungen z.B. sind mit Nummer.pdf angelegt - die rechnungsnummer kenne ich nicht von vornherein).

Zum Reader: ebenso wie der gesamte Login ist das eine Frage der Zielgruppe. Wirklich alle meiner Kunden nutzen Windows, der Großteil weiß (zu Beginn) nicht, was “Firefox” überhaupt ist - und hat den IE mit dem AdobeReader so installiert, dass PDF-Dateien direkt im Browser angezeigt werden.

Manchmal führ das zu so seltsamen Blüten wie hier: [biometrixx.de](http://www.biometrixx.de) Er wollte sich partout nicht davon überzeugen lassen, dass es keine gute Idee ist, einen kompletten Internet-Auftritt mit PDFs zu erstellen... bei ihm funktioniert es ja, alle anderen (inklusive Suchmaschinen) sind ihm wurscht. Na ja, aus der Nummer bin ich zum Glück raus :wink:

edit:
wer sich das mal “live” ansehen will: login.i-de.biz
User "Testuser"
PW “netzwerk”
(Natürlich ohne " )

Das Problem betrifft im “Dokumente” die Punkte (Unterverzeichnisse) Rechnungen (__rechnungen) und Verträge (__vertraege). Ja, die zwei Unterstriche sind gewollt :wink:

Du wirst den Dateinamen wohl über GET übergeben müssen…anders kann man das nicht zuverlässig überprüfen :neutral_face: (deine kunden würden es warscheinlich nicht schaffen einen falschen Referer zu versenden, aber ich weiß nicht wie geheim die Daten sind)

du könntest allerdings den Dateinamen in der Session speichern und dann diesen zurückgeben :wink:
Beispiel:

<?php
session_start();

if(!$_SESSION['username'])
{
   header("Location:login_error.php");
   die("Du bist hier falsche :p");
}

$_SESSION['file'][0] = "bsp.pdf"; //Dateinamen holen, von wo auch immer ;)
?>
<!-- HTML-Zeugs -->
<a href="download.php?<?php htmlspecialchars(SID); ?>&file=0">Download</a>
<!-- Rest vom HTML -->

GET Parameter kann man mit Mod-Rewrite bequem mit dem [QSA]-Switch mitnehmen :wink:

hier werden alle GET-Variablen mitgegeben :wink:

[quote]du könntest allerdings den Dateinamen in der Session speichern und dann diesen zurückgeben :wink:
[/quote]
Danke - das war der Denkanstoss, der mir gefehlt hat :smiley:
Damit ist es dann möglich, eine download.php (mit Session-Überprüfung) zu basteln, die die PDF-Datei zum Download anbietet.
Der direkte Zugriff wird dann so wie bisher auch auf die Login-Seite umgeleitet.

Das probiere ich heute noch aus - danke!

Nun, man kann ja auch ein Formular per POST senden, dann
treten GET-Parameter gar nicht auf.

Jedenfalls täte ich auch die fraglichen Dateien in ein Verzeichnis
packen und das für den direkten Zugriff komplett sperren.
Das Zielskript des Formulares kann dann sessions etc auswerten,
ebenso Angaben dazu, welche Datei rausgerückt werden soll,
was dann letztlich wie beschrieben mit readfile passieren kann
(passenden MIME-Typ senden nicht vergessen).

Zu beachten ist dabei, daß man überprüfen muß, daß die
rausgerückten Dateien auch wirklich welche sind, die man
rausrücken möchte, die Angabe von Dateinamen also auf das
entsprechende Verzeichnis beschränken und sich gut überlegen,
wer welche Dokumente einsehen darf, sonst bekommt noch
Frau Müller das Dokument für Herrn Maier heraus, wenn sie
aufgrund des eigenen Dateinamens raten kann, welches
Dokument für Herrn Müller bestimmt sein könnte ;o)
Zudem könnte Herr Spitzbub versuchen, durch irgendwelche
Tricks mit dem Formular an die PHP-Skripte selbst
heranzukommen, um diverse Dinge auszuspionieren oder aber
auch schlicht das gesamte Projekt zu schreddern, wenn er durch
das Spionieren eine weitere Sicherheitslücke findet.

[quote=“hoffmann”]Nun, man kann ja auch ein Formular per POST senden, dann
treten GET-Parameter gar nicht auf.[/quote]Ja, weiss ich. GET wird bei mir so gut wie gar nicht verwendet (nur für unkritische Dinge, wie z.B. die Sortierung des Gästebuches).

Ich habe das Problem jetzt so gelöst:
Statt (wie zuvor) Links zu den Dateien zu erzeugen, werden die Dateien jetzt in Formularen abgelegt. Ich kann also nicht mehr (wie zuvor) auf den dateinamen klicken, sondern muss die (neue) Schaltfläche “anzeigen” verwenden.
Damit werden mit POST die Daten an die download.php gesentet (Dateiname, Verzeichnispfad, Sessionname und -ID sind als “hidden fields” angelegt).

Die download.php sieht so aus:[code]<?php
session_start();
$sn = session_name();
$sid = session_id();

// Session-ID falsch oder nicht existent
if ($_POST[$sn] != $sid or !isset($_SESSION[‘username’])) {header(“Location:login_error.php”);}

else {
$file = $_POST[‘userdir’].$_POST[‘file’];
header(“content-type: application/pdf”);
header("content-lenght: ".filesize($file));
header(“content-disposition: attachment”);
readfile ($file);
}
?>[/code]
Die .htacces in den Userverzeichnissen hat sich nicht geändert und leitet mich bei Direktaufruf an die “login_error.php” weiter.

Fazit: klappt alles, wie es soll… danke nochmals an alle Beteiligten :slight_smile:


Das Thema “Sicherheit” (hier insbesondere XSS-Anfälligkeit) wird noch von mir abgeklärt - etwas später.
Momentan sind keine kritischen Daten angelegt (mit Ausnahme der Kundenrechnungen vielleicht) - und die User existieren für maximal 3-4 Monate.

Was in den versteckten Formularfeldern steht, kann sich ja
jeder im Quelltext nachsehen.

Daher solltest du hinsichtlich der Sicherheit zumindest ein
Verzeichnis fest vorgeben, in dem dann all jene Unterverzeichnisse
sind, die man gegebenenfalls angeben kann. Ferner ist es der
Angelegenheit der Sicherheit auch förderlich, wenn du in vom
Nutzer angegebenen Verzeichnisnamen und Dateinamen
mindestens alle ‘/’ etwa durch ‘§’ oder sowas ersetzt, damit man
da nicht angeben kann: ‘…/müller/rechnung.pdf’ oder
’…/…/…/geheimesSkript.php’ und dann gucken, ob es die Datei
überhaupt gibt, das übliche Spielchen eben ;o)

Wenn du von der Sitzung her schon weißt, ob es sich um
Frau Müller oder Herrn Maier handelt, ist es natürlich ungleich
besser, das Unterverzeichnis nicht über das Formular zu
übertragen, sondern direkt aus der Sitzung zu extrahieren.

[quote=“hoffmann”]Was in den versteckten Formularfeldern steht, kann sich ja
jeder im Quelltext nachsehen.[/quote]
Grundsätzlich: ja.
In diesem Fall: eher nein :wink:
Wenn meine Kunden etwas mit Quelltext anfangen könnten, würden sie mich nicht mit der Erstellung einer Webseite beauftragen. Andere erhalten keinen Zugriff auf die (User-)Seite.

[quote=“hoffmann”]Daher solltest du hinsichtlich der Sicherheit zumindest ein
Verzeichnis fest vorgeben, in dem dann all jene Unterverzeichnisse
sind, die man gegebenenfalls angeben kann.[/quote]Habe ich - siehe oben.
Mal etwas genauer:
/restricted - der komplette Userbereich
/username - enthält die (in Arbeit befindliche) Webseite und die Ordner /__rechnungen und /__vertraege.
Die Ordner mit den Unterstrichen existieren in jedem Userverzeichnis und sind mit .htaccess für den Direktzugriff gesperrt (Redirect).

[quote=“hoffmann”] Ferner ist es der
Angelegenheit der Sicherheit auch förderlich, wenn du in vom
Nutzer angegebenen Verzeichnisnamen und Dateinamen
mindestens alle ‘/’ etwa durch ‘§’ oder sowas ersetzt, damit man
da nicht angeben kann: ‘…/müller/rechnung.pdf’ oder
’…/…/…/geheimesSkript.php’ …[/quote]Genau das ist durch die .htaccess nicht möglich :wink:
Aber versuche es ruhig: login.i-de.biz/user/Testuser … ertrag.pdf

Wirklich: gemessen an der Zielgruppe denke ich, dass diese Lösung ausreichend ist.
Inwieweit jetzt böse Buben von ausserhalb (mit eigenem Script / XSS) Dummheiten anstellen können… na ja, wie gesagt: das checke ich noch.
Hauptproblem dieser Leute wird allerdings sein, die Datei- bzw. Pfadnamen richtig zu raten… denn ein Herr Müller hat nicht zwangsläufig auch das Verzeichnis /user/Mueller :smiley:

Ähm - das .htaccess betrifft nur den direkten Zugriff per HTTP.

Wenn eine Datei mittels PHP und readfile oder sowas gelesen
wird, geht das komplett an .htaccess vorbei, das ist ja gerade
der Trick, mit dem man mittels PHP den Zugriff möglich machen
kann, der direkt per HTTP nicht geht.

Wegen
$file = $_POST[‘userdir’].$_POST[‘file’];
kann der Angreifer da Beliebiges an Namen angeben, was gar
nicht weiter geprüft wird.
Noch toller wäre es mit include statt readfile und bei einem
server, bei dem es erlaubt ist, per PHP externe Inhalte
einzubinden. Dann könnte der Angreifer sogar direkt ein Skript
einschmuggeln, was etwa das gesamte Projekt rekursiv
schreddert. Ist schon manchem passiert, der ohne weitere
Prüfung Nutzereingaben zu Dateinamen verwurstet ;o)
Sollte allerdings per readfile nicht passieren, damit kann man
vermutlich ‘nur’ den Quelltext beliebiger PHP-Skripte, .ht-Dateien
etc ausspionieren, an die der Angreifer sonst nicht dran käme …

Mein erstes PHP-Projekt war ein chat, nachdem ich eine gute
Stunde gebraucht hatte, den fertigzustellen, habe ich dann mal
zum Spaß ein PHP-Kommando in das Eingabefeld geschrieben.
Gut, danach habe ich mich dann mehr als eine Stunde darum
gekümmert, Sicherheitslöcher zu schließen ;o)

Ooookey… das ist dann XSS. Und so ziemlich genau das, was nicht passieren darf.

Aber würde dieser Angriff nicht an // Session-ID falsch oder nicht existent if ($_POST[$sn] != $sid or !isset($_SESSION['username']))scheitern? Es wird ja keine (oder zumindest nicht die richtige) Session erzeugt…?


edit: falls Du den direkten (PHP-)Zugriff auf die PDFs meintest: wie kann man das dann verhindern?

Übrigens: der Dateiname wird nicht aus einer Benutzereingabe generiert :wink:

Ich würde den Dateizugriff in etwa so lösen:
Der zugriff auf den Ordner mit den Dateien wird per .htaccess komplett verhindert. kennt man den direkten Pfad, wird man ihn nicht downloaden können, auch nicht wenn der User angemeldet ist.
Stattdessen erstelle ich eine Tabelle, in der alle Dateien mit einer Eindeutigen ID gespeichert sind, und einer zusätzlichen Spalte mit den UserIDs, der Benutzer, die diese Datei herunterladen dürfen.

Aufgerufen wird Bsp, wie folgt: download?file=145
Das Script überprüft natürlich die Session, holt die Informationen aus der Tabelle mit der dazugehörigen ID, überprüft ob der User die Datei herunterladen darf, und gibt dann per PHP readfile die Datei (der Pfad zur Datei steht auch in der Tabelle) in den Browser aus.

Edit:
Das einzige was man als Codeschmuggler noch probieren könnte, wäre eine MySQL-Injektion, indem man die file-ID mit SQL Syntax bestückt.
Da das aber eine Zahl sein muss, kann man dem mit is_numeric oder preg_match entgegenwirken ohne darauf zu achten, ob magic_quotes aktiviert sind oder nicht.

mfg Balmung

Leider nicht realisierbar.
Ich weiß heute noch nicht, welche Dateien (bzw. Dateinamen) vergeben werden, und kenne auch die User (noch) nicht.

Außerdem:[quote=“hoffmann”]Wenn eine Datei mittels PHP und readfile oder sowas gelesen
wird, geht das komplett an .htaccess vorbei, das ist ja gerade
der Trick, mit dem man mittels PHP den Zugriff möglich machen
kann, der direkt per HTTP nicht geht.[/quote]
Dieses Problem würde doch dann immer noch stehen, oder?
(Prinzipiell habe ich jetzt ja bereits eine ähnliche Lösung… nur halt per Formular und hidden inputs. Sessions werden überprüft, .htaccess ist da und verhindert den direkten Zugriff - siehe oben.)

[quote=“i.deFix”]Leider nicht realisierbar.
Ich weiß heute noch nicht, welche Dateien (bzw. Dateinamen) vergeben werden, und kenne auch die User (noch) nicht.[/quote]
Irgendwann werden die Dateien doch hochgeladen oder? da werden diese dann in der Tabelle “registriert”. Zur not (fall die Dateien bsp. per ftp hochgeladen werden) könnte man noch ein Script basteln, welche den/die Ordner durchsucht, in dem die Dateien liegen, und der Admin kann dann die Dateien, welche nicht in der DB stehen, freigeben.
Und jeder andere Benutzer, darf mit seinen Dateien dann bestimmen, wer diese runterladen darf und wer nicht.
Lässt sich doch alles wunderbar lösen?

wie denn?
Die einzige Information zur Datei ist diese eine ID. Da gibt es keine Dateinamen/-pfade die man heimlich einschleußen könnte.
Man kann selbstverständlich versuchen eine andere ID einzugeben, dafür soll natürlich die Überprüfung stattfinden, ob der Benutzer die Erlaubnis hat die Datei runterzuladen.

mfg Balmung

Wenn man keine Datenbank nutzt, kann man die auch nicht
angreifen ;o) Wenn man von Datenbanken wenig Ahnung hat und
nur einfache Anwendungen wie diese, kann es also ein
Sicherheitsvorteil sein, ganz auf die Datenbank zu verzichten,
dann kann die auch nicht geschreddert werden ;o)

Wenn man nun für jeden Kunden ein Unterverzeichnis hat und
alle Unterverzeichnisse in einem Verzeichnis liegen, welches vor
dem direkten Zugriff per HTTP geschützt ist, so muß man
eigentlich nur noch verhindern, daß der jeweilige Nutzer durch
eine manipulierte Eingabe ins falsche Verzeichnis guckt.
Das kann man recht effektiv erledigen, indem man nur den
Dateinamen eingeben läßt, in diesem eben alle ‘/’ entfernt oder
ersetzt de2.php.net/manual/de/function.str-replace.php ,
das Verzeichnis automatisch anhand des identifizierten
Nutzers bestimmt und dann eben guckt, ob die Datei in dem
Verzeichnis existiert de2.php.net/manual/de/function.file-exists.php

Wenn außer dem qualifizierten und identifizierten Nutzer sowieso
niemand in das Verzeichnis gucken kann, muß der Name ja auch
nicht irgendwie kryptisch sein und kann sogar der Nutzername
selbst sein, wenn der Nutzer eben gar keine Möglichkeit hat, ein
anderes Verzeichnis anzugeben.

Wenn ich das vorliegende Skript richtig verstanden habe, wird
zwar geprüft, daß der Nutzer überhaupt was tun darf, es wird
aber nicht überprüft, was er wirklich versucht. Das schränkt den
Mißbrauch natürlich auf die Leute ein, die überhaupt Zugang zur
Seite haben, aber man glaubt es kaum, wie kreativ und fließig
einige Leute werden, wenn sie aus Frust oder Enttäuschung
plötzlich auf die Idee kommen, jemandem eins auszuwischen.
Bei den oben geschilderten einfachen Maßnahmen jedenfalls und
dem vermutlich kleineren Personenkreis von Leuten, die nicht
besonders viel Ahnung haben, maximiert sich die Chance, einen
unzufriedenen Angreifer zügig zu frustieren, da er keine
Fortschritte seiner Bemühungen sehen wird. Und wenn es keine
unzufriedenen Kunden gibt, hat man immerhin was gelernt ;o)