Einmalig gültiger Downloadlink mit PHP

Mal wieder etwas PHP-Gefrickel von mir:

if (isset($_GET['tan'])){
   $tan = preg_replace('/[^a-f0-9]/', '', $_GET['tan']);
   $check = @unlink('tan/'.$tan);
   if ($check === true) {
      $basedir = '/home/downloads/';
      $datei = 'datei.zip';
      $filename = sprintf('%s/%s', $basedir, $datei);
      header('Content-Type: application/octet-stream');
      $save_as_name = basename($datei);
      header('Content-Disposition: attachment; filename='.$save_as_name);
      readfile($filename);
   } else {
      echo 'TAN war FALSCH!';
   }
exit;
}

Dazu noch in das Verzeichnis tan einige leere Dateien mit den Dateinamen
246e238a14fcbe127c16add83492f2ff63ea63d8
b7c39fd38a24e8c5a79f89b22c890bc36b480528
ea072e3a66c938ff2a23aff99c28a11606487ca4
...

kopieren und schon können Downloadlinks nach dem folgenden Muster verteilt werden:
http://download/?tan=246e238a14fcbe127c16add83492f2ff63ea63d8


Die Aufgabenstellung war die folgende: Da möchte jemand digitale Inhalte verkaufen, diese aber erst nach erfolgreicher Bezahlung zur Verfügung stellen. Da pro Jahr nur mit einer Handvoll Bestellungen zu rechnen ist, ist eine automatische Abwicklung viel zu aufwändig. Der Versand eines Downloadlinks per E-Mail ist eine durchaus praktikable Lösung.

Meine Idee dazu ist das TAN-Verfahren wie es beim Online-Banking Verwendung findet.


Meine TAN besteht einfach aus einer leeren Datei, deren Dateiname ein Hash ist, also Ziffern von 0 bis 9 und Buchstaben von a bis f. Das preg_replace sorgt dafür, dass nichts anderes angenommen wird, als diese Zeichen. Per unlink wird die TAN bzw. die Datei gelöscht. Konnte die Datei erfolgreich gelöscht werden, so war die TAN korrekt, wenn nicht, dann eben nicht — ganz einfach. War die TAN korrekt, startet automatisch der Download. Damit der Link zur Datei zum einen nicht in dem Download-Manager des Browsers erkennbar ist und zum zweiten die Datei außerhalb von htdocs liegen kann und nur mit Script runtergeladen werden kann, habe ich mich hiervon inspirieren lassen, bzw. den größen Teil daraus entnommen: http://www.php-faq.de/q-datei-download.html


Wenn in ein paar Jahren die TANs aufgebraucht sind, dann müssen nur Dateien mit entsprechendem Dateinamen in das tan-Verzeichnis kopiert werden. :O)


Update: 04.02.2011 – 13:43 Uhr

Konstruktive Kritik und etwas Nachdenken hat nun die preAlpha-Version zur 1xDownloadLink-V.2011.02 erhoben. ;O)

if (isset($_GET['tan'])){
   $tan = preg_replace('/[^a-f0-9]/', '', $_GET['tan']);
   $check = file_exists('tan/'.$tan);
   if ($check === FALSE) {
      echo 'TAN war FALSCH!';
   } else {
      unlink('tan/'.$tan);
      header('Content-Type: application/octet-stream');
      header('Content-Disposition: attachment; filename=datei.zip');
      header('Content-Length: 2330');
      readfile('/Applications/MAMP/downloads/datei.zip');
   }
}
exit;

11 comments

  1. Hallo,

    Sehr schoenes Snipplet.

    Ich pflege da einen leicht abgeaenderten Ansatz zu verwenden. Naemlich schreibe ich die TANs in eine einzige Datei und lese diese zeilenweise mit file() ein. Danach durchsuche ich das Array, entferne die verbrauchte TAN und schreibe die neue Datei (oder flagge die TAN in einer CSV).

    Ich bin naemlich ein bisschen paranoid, wenn es um direkte Zugriffe auf das Dateisystem geht. Es ist mir einfach lieber, wenn ich stattdessen Textdateien lese/schreibe. Auch wenn rein formal gesehen beides in etwa gleich kritisch ist ;)

    Bye, Marc

  2. Das wäre auch eine Möglichkeit. Ich habe es mit einem DAU zutun, der kann jetzt mal eben in das Verzeichnis schauen und die noch zur Verfügung stehenden TANs überblicken. Das öffnen einer Datei… naja, da könnte er schon Probleme bekommen. Der öffnet sie dann mit Word, macht ein DOC-File draus, markiert die verbrauchten TANs in rot und wundert sich dann dass es nicht mehr funktioniert. :O)

  3. hm, ich sehe hier einige schwächen:

    a) konzeptionell

    1. die bereitstellung des (nur 1x mal gültigen) download-links hat nichts mit dem (nicht-)vorhandensein der eigentlichen ressource zu tun. sollte bspw. bei dem vorgang irgendwas schiefgehen, ist die datei vorloren.

    2. mir wird nicht klar, wozu du die leeren tan-dateien benötigst…

    b) coding

    1. wozu sprintf? $basedir und $datei sind hardcoded.
    2. keine prüfung, ob die schlüssel-datei tatsächlich vorhanden ist; die prüfung des rückgabewertes von readfile ist kein äquivalent dazu.
    3. der content-length header fehlt.

    alles kein problem; als “gefrickel” annehmbar, aber eben kein “sehr schoenes snipplet” .-)

    cx

  4. Du willst mir doch wohl nicht sagen dass mein (geklauter) PHP-Code, der auf Jahrhunderte währende Erfahrung und umfassenden klinischen Studien basiert (aber garantiert ohne Tierversuche), nicht absolut genial ist?

    ;O)

    Bin ich ein PHP-Coder? Njet. Ich rotz nur mal was (aus einer preAlpha-Phase, was total unausgegoren ist) in mein Blog, mehr ist dies nicht.

  5. nee, ich wollte dir lediglich durch die blume sagen: “schuster bleib bei deinen leisten” .-)

    cx

  6. Ich bin ja nicht einmal Schuster, ich bin Schlosser…

    Und was hält der PHP-Master (nicht zu verwechseln mit dem PHP-Bachelor) von diesem gefrickel:

    if (isset($_GET['tan'])){
       $tan = preg_replace('/[^a-f0-9]/', '', $_GET['tan']);
       $check = file_exists('tan/'.$tan);
       if ($check === FALSE) {
          echo 'TAN war FALSCH!';
       } else {
          unlink('tan/'.$tan);
          header('Content-Type: application/octet-stream');
          header('Content-Disposition: attachment; filename=datei.zip');
          header('Content-Length: 2330');
          readfile('/Applications/MAMP/downloads/datei.zip');
       }
    }
    exit;

    Ist aber auch noch eine frühe preAlpha(bevor-Ziffern-erfunden-wurden)-Version.

  7. passt schon.

    1. die fehlerunterdrückung per @-operator deutet i.d.r. auf ein konzeptionelles problem hin; an dieser stelle – in rahmen eines snippets – kann man’s (ungern) durchgehen lassen.

    2. das php-handbuch zu readfile: “Wenn ein Fehler auftritt wird FALSE zurückgegeben [...]” IRGEND ein fehler also; die rückkopplung mit “TAN war FALSCH!” ist daher nicht ganz sauber. wozu gibt’s file_exists…

    3. wenn sich auch in anderen verzeichnissen dateien mit derartigen namen befinden, müsste man das ganze auf directory traversal abklopfen.

    cx

  8. Wo ist da ein konzeptionelles Problem?

    (Jaha, das geht wenn man Master-of-the-eigener-Blog ist. <g>)

    preg_replace fackelt doch das Ausbrechen aus dem vorgesehenen Verzeichnis bereits ab!? Es gibt auch kein anderes Verzeichnis, mit ähnlichen Dateinamen.

  9. zum konzeptionellen: weil man schlichtweg nicht mit dem @-operator arbeitet, sondern ein vernünftiges fehler-management einbaut. denk’ mal drüber nach, wozu es fehlermeldungen gibt… diese einfach zu ignorieren, führt das ganze ad absurdum.

    zu preg_replace: stimmt :-)

    cx

  10. Fehlermeldungen deaktiviere ich in produktiven Seiten (sofern ich die Möglichkeit dazu habe) und… naja, ich bin kein Coder und mache nur mal was für Bekannte auf Zuruf. Ich habe im Posting nur die erste Idee reingehauen, auf Grundlage des fremden Codes ohne die Version so in den produktiven Einsatz zu übernehmen.

    Bei meinen Penntests finde ich oft kein gutes Fehlermanagement und dies unabhängig von der Größe des Kunden. Ich habe da immer den Eindruck: Die für-Geld-Coder achten nur darauf ob das was gewünscht ist auch funktioniert, aber nicht ob auch unerwünschte Funktionen möglich sind — dies zu prüfen ist genau mein Spaß an der Sache. :O)

  11. versteh’ schon, aber fehlercodes “unterdrücken” (@-operator) und die (optische) ausgabe von fehlercodes “deaktivieren” (error_reporting & co.) sind 2 paar schuhe.

    du sollst deinen spass haben bzw. bekommen… wollte dich sowieso bald / irgendwann mal wieder kontaktieren .-)

    cx

Comments are closed.