Content-Disposition di PHP - Nama File Download adalah Metadata, Bukan Potongan Header
Sebuah endpoint PHP sudah memilih PDF yang tepat, memeriksa bahwa pengguna saat ini boleh mengunduhnya, dan siap mengirim response. Tinggal satu pertanyaan kecil: apa nama file saat disimpan? Kita mungkin tergoda untuk menempatkan judul dari database langsung setelah filename=. Jalan pintas itu mencampur teks aplikasi dengan sintaks HTTP, dan menjadi sangat rapuh ketika nama memuat spasi, tanda kutip, karakter non-ASCII, atau baris baru.
Model mental yang lebih aman cukup sederhana: nama file download adalah metadata. Ia bukan path filesystem, bukan bukti media type payload, dan bukan potongan header yang boleh diperpanjang oleh input. Artikel ini secara khusus membahas pembuatan response field Content-Disposition di PHP. Otorisasi, resolusi path, byte range, caching, dan transfer yang efisien tetap merupakan tanggung jawab terpisah.
Satu nama melewati beberapa batas
Endpoint download yang berguna dapat melibatkan setidaknya tiga nama berbeda:
- Nama penyimpanan internal, mungkin identifier opaque seperti
8f/42/object.bin. - Nama tampilan yang disimpan aplikasi, seperti
Laporan Oktober 2026.pdf. - Nama file lokal yang pada akhirnya dipilih atau disesuaikan browser dan sistem operasi.
Nama-nama itu tidak harus sama. Memisahkannya mencegah preferensi tampilan berubah menjadi lookup penyimpanan. Aplikasi dapat mengotorisasi sebuah objek melalui identifier internalnya, memilih media type dari metadata tepercaya, lalu menawarkan nama yang mudah dipahami manusia.
RFC 6266 mendefinisikan Content-Disposition sebagai informasi tambahan tentang bagaimana payload response sebaiknya diproses. Field ini juga dapat membawa nama file yang disarankan untuk menyimpan payload tersebut. Kata disarankan penting: penerima harus memperlakukan nama ini sebagai petunjuk, bukan sebagai kewenangan atas filesystem lokal.
Attachment adalah petunjuk, bukan kendali jarak jauh
Dua disposition type yang umum adalah inline dan attachment. Menurut RFC 6266, inline menyiratkan pemrosesan normal, sedangkan attachment menunjukkan bahwa penerima sebaiknya meminta pengguna menyimpan response secara lokal. Type yang tidak dikenal sebaiknya diperlakukan seperti attachment.
Content-Type: application/pdf
Content-Disposition: attachment; filename="report.pdf"
Ini tidak berarti server mengendalikan setiap keputusan user interface. Browser dapat menerapkan kebijakan keamanan, preferensi pengguna, atau kebiasaan platformnya sendiri. attachment juga tidak memperbaiki Content-Type yang keliru. Disposition menjelaskan cara penanganan yang dimaksud, sedangkan media type menjelaskan representasinya. Endpoint yang andal mengirim keduanya secara sengaja.
Response field ini juga berbeda dari field Content-Disposition di dalam multipart/form-data. RFC 6266 secara eksplisit mengecualikan field di dalam payload body tersebut dari cakupannya. Menggunakan ulang contoh antara parsing upload dan response download tanpa memeriksa spesifikasi yang berlaku dapat menghasilkan asumsi yang salah.
Mengapa ada dua parameter filename
Parameter konvensional filename bekerja baik sebagai fallback ASCII yang konservatif. Quoted string diperlukan ketika nilainya mengandung spasi. Memasukkan byte UTF-8 langsung ke quoted string itu bukan mekanisme internasionalisasi interoperable yang didefinisikan standar.
Parameter extended filename* membawa character encoding, bahasa opsional, dan nilai yang di-encode. RFC 6266 awalnya merujuk RFC 5987 untuk format ini; RFC 8187 kemudian menggantikan RFC 5987 dan mengharuskan produsen memakai UTF-8. Nama seperti laporan-äkhir.pdf dapat direpresentasikan sebagai:
Content-Disposition: attachment;
filename="laporan-akhir.pdf";
filename*=UTF-8''laporan-%C3%A4khir.pdf
Dua apostrof setelah UTF-8 membiarkan komponen bahasa opsional tetap kosong. Nilai extended tidak diapit tanda kutip. Byte yang tidak boleh ditulis langsung diubah dengan percent-encoding, dan spasi menjadi %20, bukan +.
Ketika kedua parameter ada, RFC 6266 menyatakan penerima sebaiknya memilih filename* dan mengabaikan filename. Nilai ASCII tetap menjadi fallback kompatibilitas. Saran pembuatan dalam RFC menempatkan fallback itu lebih dahulu karena beberapa implementasi lama memiliki masalah parsing jika urutannya dibalik. Pasangan ini menghindari tebakan berdasarkan nama user agent.
Validasi sebelum encoding
Encoding bukan validasi. Percent-encoding dapat menserialisasi Unicode, tetapi tidak menentukan apakah suatu nama pantas dipakai. RFC 8187 juga mencatat bahwa Unicode dapat memungkinkan spoofing visual: dua nama bisa tampak mirip secara menipu meskipun berisi karakter berbeda.
Aplikasi sebaiknya mendefinisikan kebijakan nama file sebelum nilai tersebut mencapai API header. Kebijakan sederhana untuk download yang dihasilkan aplikasi dapat mencakup keputusan berikut:
- Hanya menerima UTF-8 valid dengan jumlah byte yang dibatasi.
- Menolak control character, termasuk carriage return dan line feed.
- Menolak
/dan\agar nama tampilan tidak menyerupai path. - Memilih extension yang sesuai dengan payload type tepercaya, bukan mempertahankan extension sembarang dari input.
- Menghasilkan fallback ASCII yang tidak kosong, sekalipun nama asli tidak memiliki huruf ASCII.
Batas panjang yang tepat adalah keputusan aplikasi. RFC 9110 tidak mendefinisikan satu batas panjang field universal; server, proxy, framework, dan client memiliki batas praktis. Karena itu, batas tersebut perlu didokumentasikan dan diuji pada deployment nyata, bukan disajikan sebagai konstanta HTTP.
Helper PHP dengan cakupan yang sengaja dibatasi
Helper berikut menerima nama tampilan yang sudah diotorisasi dan dipilih aplikasi. Batas 180 byte adalah kebijakan lokal untuk contoh ini, bukan persyaratan RFC. Ia menolak separator path dan control character, membuat fallback ASCII sederhana, lalu menserialisasi nama UTF-8 asli sebagai 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() berguna di sini karena output-nya merupakan subset konservatif yang valid dari karakter yang diperbolehkan dalam extended value RFC 8187: spasi menjadi %20, dan octet UTF-8 diubah dengan percent-encoding. Penggantian pada fallback sengaja dibuat sederhana, bukan canggih secara linguistik. Transliteration bergantung pada locale; fallback generik tetapi jujur lebih aman daripada berpura-pura bahwa satu pemetaan benar untuk setiap bahasa.
Pemanggilan terakhir memakai API header mentah milik PHP. Manual PHP menyatakan bahwa header() harus dijalankan sebelum output aktual. Byte-order mark, whitespace di luar tag PHP, atau output diagnostik yang lebih awal dapat merusak response bahkan sebelum penanganan nama file dipertimbangkan.
Hal yang tidak diselesaikan helper ini
Helper ringkas dapat memperjelas satu batas, tetapi tidak seharusnya mengumpulkan keputusan kepercayaan yang tidak berkaitan.
Pertama, helper ini tidak mengubah nama menjadi path. Endpoint seharusnya me-resolve objek yang sudah diotorisasi secara terpisah; menggabungkan nama tampilan ini ke direktori penyimpanan akan merusak pemisahan tersebut. Kedua, helper tidak memeriksa payload atau memilih media type yang aman. Jika aplikasi membiarkan pengguna memilih extension executable yang menyesatkan untuk byte sembarang, header yang valid secara sintaks tidak membuat download tersebut jinak.
Ketiga, helper tidak menjamin nama lokal akhir. RFC 6266 menginstruksikan penerima untuk membuang informasi path, mempertimbangkan keamanan extension, mengganti karakter yang membingungkan, dan menangani nama khusus seperti . serta ... Browser dan sistem operasi dapat menambahkan aturan lain. Validasi di sisi pengirim mengurangi ambiguitas; ia tidak dapat menggantikan keamanan di sisi penerima.
Terakhir, ini bukan download controller yang lengkap. Authentication, authorization, kebijakan cache, Content-Length, conditional request, range handling, logging, dan error transfer membutuhkan desainnya sendiri. Untuk file besar atau privat, mendelegasikan transfer ke web server yang mampu setelah aplikasi mengotorisasi request mungkin lebih baik daripada mengalirkan setiap byte melalui PHP.
Uji response yang benar-benar diserialisasi
Unit test sebaiknya memeriksa nilai field secara persis untuk contoh ASCII dan non-ASCII, lalu menolak baris baru, separator, UTF-8 rusak, nilai kosong, dan nama yang melewati batas pilihan. Integration test perlu memeriksa response yang keluar dari stack deployment:
curl -sS -D - -o /dev/null \
'https://example.test/download/42'
Periksa satu field Content-Disposition yang valid, disposition type yang dimaksud, fallback ASCII, extended value UTF-8, dan Content-Type yang sesuai dengan representasi. Uji melalui reverse proxy yang sama dengan production. Browser test kemudian dapat memastikan perilaku yang terlihat pengguna, tetapi jangan menjadikannya pengganti assertion tingkat protokol: browser mungkin diam-diam memulihkan sintaks rusak yang ditolak client lain.
Kesimpulan
Bagian sulit dari nama file download bukanlah memanggil header(). Tantangannya adalah menjaga batas antara teks aplikasi dan sintaks protokol. Pilih dan otorisasi objek secara terpisah, validasi nama tampilan UTF-8 yang dibatasi, tawarkan filename ASCII yang konservatif, bawa nama internasional sebenarnya dalam filename*, lalu verifikasi response yang diserialisasi.
Meski begitu, nama file tetap merupakan saran. Keterbatasan itu bukan cacat yang perlu dicari jalan keluarnya; justru itulah yang memungkinkan penerima melindungi filesystem-nya sendiri dan menghormati pilihan pengguna. Endpoint download yang baik memberi saran secara presisi tanpa berpura-pura mengendalikan sisi lain koneksi.
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.
