Fitur upload sering dianggap pekerjaan kecil: pengguna memilih file, menekan tombol, lalu aplikasi menyimpannya. Masalahnya, server tidak tahu apakah file bernama foto.jpg benar-benar gambar, apakah ukurannya wajar, atau apakah namanya mencoba menyisipkan jalur direktori.
Jika validasinya lemah, fitur upload dapat menjadi jalur untuk mengunggah file berbahaya, memenuhi penyimpanan server, menimpa file lain, atau bahkan menjalankan kode di server. OWASP memasukkan unrestricted file upload sebagai risiko serius karena dampaknya bergantung pada bagaimana file disimpan dan diproses aplikasi.
Prinsip utamanya sederhana: jangan memperlakukan file dari pengguna sebagai file yang tepercaya. Periksa siapa yang mengunggah, apa jenis file yang dibutuhkan, seberapa besar ukurannya, bagaimana isinya, dan di mana file tersebut disimpan.
Mengapa memeriksa ekstensi saja tidak cukup?
Pemeriksaan seperti pathinfo($filename, PATHINFO_EXTENSION) memang berguna, tetapi bukan pertahanan lengkap. Nama file berasal dari pengguna dan dapat dibuat dengan banyak variasi, misalnya ekstensi ganda, karakter khusus, atau nama yang mencoba keluar dari direktori upload.
Header Content-Type dari browser juga tidak boleh dijadikan sumber kebenaran tunggal. Nilainya dapat dipalsukan. Karena itu, aplikasi perlu membandingkan beberapa sinyal: ekstensi yang diizinkan, tipe MIME yang terdeteksi dari isi file, ukuran file, serta aturan bisnis aplikasi.
Contohnya, jika website hanya menerima foto profil, mungkin tidak ada alasan untuk mengizinkan PDF, ZIP, SVG, atau file berbasis skrip. Semakin sedikit jenis file yang diterima, semakin kecil permukaan serangannya.
Lapisan pemeriksaan yang sebaiknya dipakai
1. Batasi pengguna dan kebutuhan bisnis
Tentukan siapa yang boleh mengunggah file. Form upload untuk administrator tentu berbeda dengan form lampiran yang dapat diakses publik. Terapkan autentikasi, otorisasi, dan perlindungan CSRF pada endpoint upload.
Setelah itu, buat daftar izin yang sempit. Untuk avatar, cukup izinkan format gambar yang benar-benar dipakai. Untuk dokumen, mungkin hanya PDF atau DOCX. Jangan membuat aturan berbasis “blokir ekstensi berbahaya” saja, karena pendekatan tersebut mudah melewatkan variasi ekstensi lain.
2. Batasi ukuran dan jumlah file
Ukuran file perlu dibatasi di lebih dari satu tempat: validasi aplikasi, konfigurasi PHP seperti upload_max_filesize dan post_max_size, serta batas pada web server atau proxy jika tersedia.
Batasi juga jumlah file dalam satu permintaan dan jumlah upload per pengguna. Tanpa pembatasan, penyerang tidak perlu menjalankan kode untuk membuat masalah; mereka cukup menghabiskan kapasitas disk atau memaksa server memproses file yang sangat besar.
3. Deteksi isi file, bukan hanya namanya
PHP menyediakan finfo_file() untuk membaca informasi tipe file dari isinya. Ini lebih baik daripada mempercayai nilai dari browser, tetapi tetap bukan jaminan mutlak. Untuk gambar, lakukan pemeriksaan tambahan dengan mencoba membaca atau memproses gambar menggunakan pustaka yang sesuai.
Validasi juga harus sesuai kebutuhan. File yang akan ditampilkan kembali di browser perlu dipertimbangkan risikonya terhadap content sniffing, XSS, dan format aktif seperti SVG. Jika fitur tidak memerlukan SVG, lebih aman tidak mengizinkannya.
4. Buat nama file baru
Jangan menyimpan file menggunakan nama asli pengguna sebagai nama final di server. Nama tersebut bisa mengandung karakter yang tidak diharapkan, benturan dengan file lain, atau pola traversal seperti ../.
Gunakan nama acak yang dibuat aplikasi, lalu simpan ekstensi yang sudah ditentukan dari hasil validasi. Nama asli dapat disimpan sebagai metadata jika memang perlu ditampilkan kepada pengguna.
<?php
$allowed = ['image/jpeg', 'image/png'];
$maxSize = 2 * 1024 * 1024;
if ($_FILES['avatar']['error'] !== UPLOAD_ERR_OK) {
throw new RuntimeException('Upload gagal.');
}
if ($_FILES['avatar']['size'] > $maxSize) {
throw new RuntimeException('Ukuran file terlalu besar.');
}
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($_FILES['avatar']['tmp_name']);
if (!in_array($mime, $allowed, true)) {
throw new RuntimeException('Jenis file tidak diizinkan.');
}
$extension = $mime === 'image/png' ? 'png' : 'jpg';
$filename = bin2hex(random_bytes(16)) . '.' . $extension;
$target = __DIR__ . '/private-uploads/' . $filename;
if (!move_uploaded_file($_FILES['avatar']['tmp_name'], $target)) {
throw new RuntimeException('File tidak dapat disimpan.');
}
?>Contoh tersebut belum mencakup seluruh kebutuhan produksi, tetapi menunjukkan pola penting: cek status upload, ukuran, tipe file, gunakan nama acak, lalu pindahkan file dengan move_uploaded_file(). Fungsi tersebut memastikan sumber file berasal dari mekanisme upload HTTP PHP, tetapi keamanan tujuan penyimpanan tetap menjadi tanggung jawab aplikasi.
Di mana file sebaiknya disimpan?
Pilihan paling aman adalah menyimpan file di luar direktori yang dapat menjalankan kode. Jika memungkinkan, simpan di luar web root dan layani file melalui endpoint yang memeriksa hak akses terlebih dahulu.
Untuk file yang memang harus publik, pastikan direktori upload tidak mengizinkan eksekusi skrip. Jangan mengandalkan perubahan nama file saja. Konfigurasi web server tetap perlu mencegah file upload diperlakukan sebagai kode.
File juga sebaiknya tidak langsung dipercaya untuk diproses oleh komponen lain. PDF, gambar, arsip, dan dokumen Office dapat membawa risiko berbeda. Pada sistem dengan kebutuhan keamanan lebih tinggi, pertimbangkan pemindaian antivirus atau analisis file sebelum file tersedia bagi pengguna lain.
Catatan khusus untuk WordPress
WordPress sudah menyediakan alur upload seperti wp_handle_upload() yang melakukan beberapa pemeriksaan, termasuk status upload, ukuran, dan tipe file. WordPress juga menyediakan filter wp_handle_upload_prefilter untuk memeriksa atau mengubah data file sebelum dipindahkan.
Namun, plugin atau tema tetap dapat membuka jalur baru yang lebih lemah. Hindari menulis ulang proses upload jika fungsi bawaan WordPress sudah mencukupi. Jika harus membuat endpoint sendiri, gunakan pemeriksaan nonce, cek kemampuan pengguna dengan current_user_can(), batasi tipe file, dan jangan mengubah daftar MIME yang diizinkan tanpa alasan yang jelas.
Perhatikan pula plugin yang menerima ZIP, CSV, atau file impor. Arsip dapat berisi banyak file, jalur direktori yang tidak aman, atau ukuran hasil ekstraksi yang jauh lebih besar daripada ukuran ZIP-nya. Jangan langsung mengekstrak arsip tanpa memeriksa isi dan target direktorinya.
Checklist yang bisa dilakukan sekarang
- Tentukan daftar ekstensi dan MIME yang benar-benar diperlukan.
- Tolak file yang melebihi batas ukuran, jumlah, atau kuota pengguna.
- Jangan percaya nama file dan
Content-Typedari browser. - Deteksi tipe file dari isinya dan lakukan pemeriksaan khusus untuk gambar atau arsip.
- Gunakan nama file acak yang dibuat aplikasi.
- Simpan file di luar web root jika file tidak harus publik.
- Matikan eksekusi skrip pada direktori upload.
- Pastikan endpoint upload memiliki autentikasi, otorisasi, dan perlindungan CSRF.
- Catat siapa yang mengunggah file, kapan, jenisnya, dan apakah pemeriksaannya berhasil.
- Uji upload dengan nama aneh, ekstensi ganda, file kosong, file terlalu besar, dan tipe file yang tidak sesuai.
Apa artinya bagi kita?
Keamanan upload bukan soal menemukan satu fungsi PHP yang ajaib. Ini soal mengurangi kepercayaan secara bertahap: batasi siapa yang boleh mengunggah, terima hanya file yang dibutuhkan, validasi isi dan ukurannya, simpan dengan nama baru, lalu tempatkan file di lokasi yang tidak mudah dieksekusi.
Untuk website kecil, beberapa langkah dasar tersebut sudah memberi perbaikan besar. Untuk aplikasi yang menerima dokumen sensitif atau upload publik, tambahkan pemindaian file, kuota, logging, dan proses karantina sebelum file dipublikasikan. Yang penting, perlakukan setiap file upload sebagai input yang belum dipercaya sampai seluruh pemeriksaan selesai.
Sumber & bacaan lebih lanjut
- OWASP File Upload Cheat Sheet
- OWASP Unrestricted File Upload
- PHP Manual: finfo_file
- PHP Manual: move_uploaded_file
- WordPress Developer Reference: _wp_handle_upload()
- WordPress Developer Reference: wp_handle_upload_prefilter
– Rio Yotto @rioyotto
