Content-Disposition in PHP - Ein Download-Dateiname ist Metadaten, kein Header-Fragment
Ein PHP-Endpunkt hat das richtige PDF ausgewählt, geprüft, ob der aktuelle Benutzer es herunterladen darf, und ist bereit, die Antwort zu senden. Eine kleine Frage bleibt: Wie soll die gespeicherte Datei heißen? Es liegt nahe, einen Datenbanktitel direkt hinter filename= einzusetzen. Diese Abkürzung vermischt Anwendungstext mit HTTP-Syntax und wird besonders anfällig, wenn der Name Leerzeichen, Anführungszeichen, Nicht-ASCII-Zeichen oder Zeilenumbrüche enthält.
Das sicherere Denkmodell ist einfach: Ein Download-Dateiname ist Metadaten. Er ist kein Dateisystempfad, kein Nachweis für den Medientyp des Payloads und kein Header-Fragment, das Eingaben erweitern dürfen. Dieser Artikel betrachtet gezielt die Erzeugung des Response-Feldes Content-Disposition in PHP. Autorisierung, Pfadauflösung, Byte-Ranges, Caching und effizienter Transfer bleiben getrennte Aufgaben.
Ein Name überschreitet mehrere Grenzen
Bei einem sinnvollen Download-Endpunkt können mindestens drei verschiedene Namen beteiligt sein:
- Ein interner Speichername, möglicherweise ein opaker Bezeichner wie
8f/42/object.bin. - Ein von der Anwendung gespeicherter Anzeigename wie
Laporan Oktober 2026.pdf. - Ein lokaler Dateiname, den Browser und Betriebssystem letztlich auswählen oder anpassen.
Diese Namen müssen nicht identisch sein. Ihre Trennung verhindert, dass eine Anzeigepräferenz zu einem Speicher-Lookup wird. Die Anwendung kann ein Objekt über seinen internen Bezeichner autorisieren, einen Medientyp aus vertrauenswürdigen Metadaten auswählen und erst danach einen menschenlesbaren Namen anbieten.
RFC 6266 definiert Content-Disposition als zusätzliche Information darüber, wie der Payload einer Antwort verarbeitet werden soll. Das Feld kann außerdem einen vorgeschlagenen Dateinamen zum lokalen Speichern dieses Payloads enthalten. Das Wort vorgeschlagen ist wichtig: Der Empfänger muss diesen Namen als Hinweis behandeln, nicht als Autorität über ein lokales Dateisystem.
Attachment ist ein Hinweis, keine Fernsteuerung
Die beiden bekannten Disposition-Typen sind inline und attachment. Nach RFC 6266 bedeutet inline die normale Verarbeitung, während attachment angibt, dass der Empfänger den Benutzer zum lokalen Speichern auffordern sollte. Ein unbekannter Typ sollte wie attachment behandelt werden.
Content-Type: application/pdf
Content-Disposition: attachment; filename="report.pdf"
Das bedeutet nicht, dass der Server jede Entscheidung der Benutzeroberfläche kontrolliert. Ein Browser kann eigene Sicherheitsrichtlinien, Benutzereinstellungen oder Plattformkonventionen anwenden. Ebenso korrigiert attachment keinen unzutreffenden Content-Type. Die Disposition beschreibt die beabsichtigte Behandlung; der Medientyp beschreibt die Repräsentation. Ein zuverlässiger Endpunkt sendet beides bewusst.
Dieses Response-Feld unterscheidet sich außerdem von den Content-Disposition-Feldern innerhalb von multipart/form-data. RFC 6266 schließt diese Felder im Payload-Body ausdrücklich aus seinem Geltungsbereich aus. Beispiele zwischen Upload-Verarbeitung und Download-Antworten zu übernehmen, ohne die jeweils geltende Spezifikation zu prüfen, kann daher zu falschen Annahmen führen.
Warum es zwei filename-Parameter gibt
Der herkömmliche Parameter filename eignet sich gut als konservativer ASCII-Fallback. Ein Quoted String ist nötig, wenn der Wert Leerzeichen enthält. UTF-8-Bytes einfach in diesen Quoted String einzusetzen, ist nicht der interoperable Internationalisierungsmechanismus des Standards.
Der erweiterte Parameter filename* enthält eine Zeichenkodierung, eine optionale Sprache und einen kodierten Wert. RFC 6266 verwies für dieses Format ursprünglich auf RFC 5987; RFC 8187 löste RFC 5987 später ab und verlangt von Erzeugern UTF-8. Ein Name wie laporan-äkhir.pdf lässt sich so darstellen:
Content-Disposition: attachment;
filename="laporan-akhir.pdf";
filename*=UTF-8''laporan-%C3%A4khir.pdf
Die beiden Apostrophe nach UTF-8 lassen die optionale Sprachkomponente leer. Der erweiterte Wert steht nicht in Anführungszeichen. Bytes, die nicht direkt erlaubt sind, werden prozentkodiert; ein Leerzeichen wird zu %20, nicht zu +.
Wenn beide Parameter vorhanden sind, sollen Empfänger nach RFC 6266 filename* bevorzugen und filename ignorieren. Der ASCII-Wert bleibt ein Kompatibilitäts-Fallback. Die Erzeugungshinweise des RFC setzen diesen Fallback an die erste Stelle, weil einige ältere Implementierungen bei umgekehrter Reihenfolge Parsing-Probleme hatten. Mit diesem Paar ist kein Raten anhand des User-Agent-Namens nötig.
Vor dem Kodieren validieren
Kodierung ist keine Validierung. Percent-Encoding kann Unicode serialisieren, entscheidet aber nicht, ob ein Name angemessen ist. RFC 8187 weist zudem darauf hin, dass Unicode visuelles Spoofing ermöglichen kann: Zwei Namen können täuschend ähnlich aussehen und dennoch unterschiedliche Zeichen enthalten.
Eine Anwendung sollte eine Dateinamenrichtlinie festlegen, bevor der Wert die Header-API erreicht. Eine überschaubare Richtlinie für erzeugte Downloads könnte folgende Entscheidungen enthalten:
- Nur gültiges UTF-8 mit einer begrenzten Byteanzahl akzeptieren.
- Steuerzeichen einschließlich Carriage Return und Line Feed ablehnen.
- Sowohl
/als auch\ablehnen, damit der Anzeigename keinem Pfad ähnelt. - Eine Erweiterung wählen, die zum vertrauenswürdigen Payload-Typ passt, statt eine beliebige übermittelte Erweiterung beizubehalten.
- Einen nicht leeren ASCII-Fallback erzeugen, selbst wenn der ursprüngliche Name keine ASCII-Buchstaben enthält.
Die genaue Längengrenze ist eine Anwendungsentscheidung. RFC 9110 definiert keine universelle Feldlängengrenze; Server, Proxys, Frameworks und Clients haben praktische Grenzen. Eine Grenze sollte deshalb für die tatsächliche Bereitstellung dokumentiert und getestet werden, statt sie als HTTP-Konstante darzustellen.
Ein bewusst begrenzter PHP-Helper
Der folgende Helper akzeptiert einen bereits autorisierten und von der Anwendung ausgewählten Anzeigenamen. Seine Grenze von 180 Bytes ist eine lokale Richtlinie dieses Beispiels, keine RFC-Anforderung. Er lehnt Pfadtrennzeichen und Steuerzeichen ab, erzeugt einen einfachen ASCII-Fallback und serialisiert den ursprünglichen UTF-8-Namen als filename*.
<?php
declare(strict_types=1);
function attachmentDisposition(string $displayName): string
{
if ($displayName === '' || strlen($displayName) > 180) {
throw new InvalidArgumentException('Invalid filename length');
}
if (preg_match('//u', $displayName) !== 1) {
throw new InvalidArgumentException('Filename must be valid UTF-8');
}
if (
preg_match('/[\x00-\x1F\x7F]/u', $displayName) === 1
|| str_contains($displayName, '/')
|| str_contains($displayName, '\\')
) {
throw new InvalidArgumentException('Filename contains a forbidden character');
}
$fallback = preg_replace('/[^A-Za-z0-9._ -]/', '_', $displayName);
$fallback = trim((string) $fallback, " .");
if ($fallback === '' || $fallback === '..') {
$fallback = 'download';
}
$fallback = addcslashes($fallback, "\\\"");
return 'attachment; filename="' . $fallback . '"; filename*=UTF-8\'\''
. rawurlencode($displayName);
}
$downloadName = 'Laporan Oktober 2026.pdf';
header('Content-Type: application/pdf');
header('Content-Disposition: ' . attachmentDisposition($downloadName));
// Transfer the already authorized PDF here.
rawurlencode() ist hier nützlich, weil seine Ausgabe eine gültige konservative Teilmenge der Zeichen bildet, die in einem erweiterten Wert nach RFC 8187 erlaubt sind: Leerzeichen werden zu %20, UTF-8-Oktette werden prozentkodiert. Die Ersetzung im Fallback ist bewusst schlicht statt sprachlich ausgefeilt. Transliteration hängt von der Locale ab; ein generischer, aber ehrlicher Fallback ist sicherer als die Annahme, eine Zuordnung sei für jede Sprache richtig.
Der letzte Aufruf verwendet PHPs rohe Header-API. Das PHP-Handbuch hält fest, dass header() vor jeder tatsächlichen Ausgabe aufgerufen werden muss. Eine Byte-Order-Mark, Whitespace außerhalb von PHP-Tags oder eine frühere Diagnoseausgabe kann die Antwort daher beschädigen, bevor die Behandlung des Dateinamens überhaupt relevant wird.
Was dieser Helper nicht löst
Ein kompakter Helper kann eine Grenze klarer machen, sollte aber keine unabhängigen Vertrauensentscheidungen ansammeln.
Erstens wandelt er einen Namen nicht in einen Pfad um. Der Endpunkt sollte ein autorisiertes Objekt unabhängig auflösen; diesen Anzeigenamen an ein Speicherverzeichnis anzuhängen, würde diese Trennung aufheben. Zweitens untersucht er den Payload nicht und wählt keinen sicheren Medientyp. Wenn eine Anwendung Benutzer eine irreführende ausführbare Erweiterung für beliebige Bytes wählen lässt, machen syntaktisch gültige Header den Download nicht harmlos.
Drittens garantiert er nicht den endgültigen lokalen Namen. RFC 6266 weist Empfänger an, Pfadinformationen zu verwerfen, die Sicherheit von Erweiterungen zu berücksichtigen, verwirrende Zeichen zu ersetzen und besondere Namen wie . und .. zu behandeln. Browser und Betriebssysteme können weitere Regeln hinzufügen. Senderseitige Validierung reduziert Mehrdeutigkeit; sie kann die Sicherheit auf Empfängerseite nicht ersetzen.
Schließlich ist dies kein vollständiger Download-Controller. Authentifizierung, Autorisierung, Cache-Richtlinien, Content-Length, bedingte Anfragen, Range-Verarbeitung, Logging und Transferfehler benötigen eigene Entwürfe. Bei großen oder privaten Dateien kann es sinnvoller sein, den Transfer nach der Autorisierung durch die Anwendung an einen geeigneten Webserver zu delegieren, statt jedes Byte durch PHP zu streamen.
Die serialisierte Antwort testen
Ein Unit-Test sollte den exakten Feldwert für ASCII- und Nicht-ASCII-Beispiele prüfen und anschließend Zeilenumbrüche, Trennzeichen, fehlerhaftes UTF-8, leere Werte sowie Namen oberhalb der gewählten Grenze ablehnen. Ein Integrationstest sollte die Antwort untersuchen, die den bereitgestellten Stack tatsächlich verlässt:
curl -sS -D - -o /dev/null \
'https://example.test/download/42'
Zu prüfen sind genau ein gültiges Content-Disposition-Feld, der beabsichtigte Disposition-Typ, der ASCII-Fallback, der erweiterte UTF-8-Wert und ein zur Repräsentation passender Content-Type. Der Test sollte über denselben Reverse Proxy wie in der Produktion laufen. Ein Browser-Test kann anschließend das sichtbare Verhalten bestätigen, sollte aber Assertions auf Protokollebene nicht ersetzen: Ein Browser kann fehlerhafte Syntax stillschweigend korrigieren, die ein anderer Client ablehnt.
Fazit
Der schwierige Teil eines Download-Dateinamens ist nicht der Aufruf von header(). Es geht darum, die Grenze zwischen Anwendungstext und Protokollsyntax zu erhalten. Das Objekt sollte unabhängig ausgewählt und autorisiert, ein begrenzter UTF-8-Anzeigename validiert, ein konservatives ASCII-filename angeboten, der tatsächliche internationalisierte Name in filename* übertragen und die serialisierte Antwort geprüft werden.
Auch dann bleibt der Dateiname ein Hinweis. Diese Einschränkung ist kein Fehler, den man umgehen müsste; sie ermöglicht dem Empfänger, sein eigenes Dateisystem zu schützen und die Entscheidung des Benutzers zu respektieren. Ein guter Download-Endpunkt macht einen präzisen Vorschlag, ohne vorzugeben, die andere Seite der Verbindung zu kontrollieren.
References
- Reschke, J. RFC 6266: Use of the Content-Disposition Header Field in the Hypertext Transfer Protocol (HTTP). IETF / RFC Editor, June 2011.
- Reschke, J. RFC 8187: Indicating Character Encoding and Language for HTTP Header Field Parameters. IETF / RFC Editor, September 2017.
- Fielding, R., Nottingham, M., and Reschke, J. RFC 9110: HTTP Semantics. IETF / RFC Editor, June 2022.
- PHP Documentation Group. PHP Manual: header. Accessed October 18, 2026.
