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 derrealpath()-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, nimminlinestattattachment.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
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 →Verwandte Snippets
Beträge und Zahlen in PHP formatieren – number_format, sprintf und Intl
1234.5 ist keine Anzeige, sondern ein Wert. Auf der Rechnung soll 1.234,50 € stehen, in der API 1234.50 und im Dateinamen 1234-50. Drei Aufgaben, drei Werkzeuge – und ein Fehler, den fast jeder einmal macht.
Benannte Argumente in PHP 8 – Schluss mit true, false, null, true
Ein Aufruf wie erstelle($a, true, false, null, true) sagt nichts darüber, was die Wahrheitswerte bedeuten. Mit benannten Argumenten steht es im Aufruf – und optionale Parameter lassen sich überspringen.
Reguläre Ausdrücke in PHP – benannte Gruppen statt $m[3]
Ein Treffer-Array aus Zahlen liest sich nach zwei Wochen wie eine Geheimschrift. Benannte Gruppen geben jedem Teil einen Namen – und preg_replace_callback ersetzt damit Dinge, die kein einfacher Ersetzungsstring kann.