MWCodebymw.de ↗
PHP

Dateien sicher zum Download ausliefern in PHP – mit readfile() und den richtigen Headern

Ein Download-Link auf eine private Datei ist schnell gefährlich – Pfad-Tricks führen sonst zu fremden Dateien. So lieferst du Downloads sauber aus: geschützt vor „../", speicherschonend und mit korrektem Dateinamen.

Ein häufiger Wunsch: Nutzer sollen eine Datei herunterladen, die nicht frei im Web-Root liegt – eine Rechnung, ein Export, ein gekauftes PDF. Der naive Weg (download.php?file=... und die Datei einfach durchreichen) ist eine klassische Lücke: Mit ?file=../../config.php liest jemand plötzlich deine Geheimnisse. So mache ich es sicher.

Der sichere Handler

<?php
$dir = __DIR__ . '/private-files/';

// 1) NUR den Dateinamen nehmen – basename() wirft jedes "../" weg.
$name = basename((string)($_GET['file'] ?? ''));
$path = $dir . $name;

// 2) Wirklich prüfen, dass die Datei existiert und IM erlaubten Ordner liegt.
$real = realpath($path);
if ($real === false || !str_starts_with($real, realpath($dir) . DIRECTORY_SEPARATOR)) {
    http_response_code(404);
    exit('Nicht gefunden.');
}

// 3) Header setzen – Typ, Dateiname, Größe.
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $name . '"');
header('Content-Length: ' . filesize($real));
header('X-Content-Type-Options: nosniff');

// 4) Streamen statt in den Speicher laden.
readfile($real);
exit;

Warum genau diese Schritte

  • basename() + realpath()-Check: Der doppelte Schutz gegen *Path Traversal*. basename() entfernt Pfadanteile, und der realpath()-Vergleich stellt sicher, dass die aufgelöste Datei wirklich in deinem Ordner liegt – nicht per Symlink woanders.
  • Content-Disposition: attachment: Erzwingt den Download-Dialog statt Anzeige im Browser. Willst du z. B. ein PDF *im* Browser zeigen, nimm inline statt attachment.
  • readfile(): Schaufelt die Datei blockweise raus, statt sie komplett in den RAM zu laden – so funktioniert auch ein 500-MB-Download ohne Speicherfehler.
  • X-Content-Type-Options: nosniff: Verhindert, dass der Browser den Inhaltstyp „errät".

Bei sehr großen Dateien lohnt sich vorher noch if (ob_get_level()) ob_end_clean();, damit kein Output-Buffer die Datei zwischenspeichert.

Genau nach diesem Muster liefert mein CMS geschützte Kunden-Downloads aus. Wenn du so etwas brauchst, meld dich über bymw.de.

Quellen

#Download#PHP#Sicherheit#readfile#Header

Du brauchst mehr als ein Snippet?

Ich entwickle Android-Apps in Kotlin und moderne Websites für Selbstständige und kleine Unternehmen — von der ersten Idee bis zum Release.

Projekt anfragen →