Cara Membuat Upload File dengan PHP dan Validasi Aman

Form upload sering jadi bagian paling cepat ditulis dan paling jarang diuji. Akibatnya, file yang seharusnya gambar bisa berisi skrip, ukuran file melebihi kuota hosting, atau nama file bertabrakan dan saling menimpa. Panduan ini membahas cara membuat upload file dengan PHP yang aman, mulai dari struktur folder, validasi, sampai penanganan error yang bisa dibaca pengguna.

Menyiapkan folder penyimpanan di luar public_html

File yang diunggah sebaiknya tidak disimpan di folder yang bisa diakses langsung lewat URL. Simpan di folder khusus, misalnya storage/uploads, yang berada satu level di atas public_html. Dengan begitu, file PHP yang lolos validasi tidak bisa dieksekusi dari browser.

Buat struktur seperti ini:

/var/www/
  storage/
    uploads/
  public_html/
    upload.php
    index.php

Folder storage/uploads diberi izin tulis untuk user web server, bukan 777. Di server Linux, cukup chmod 755 pada folder dan chmod 644 pada file yang diunggah.

Membuat form HTML yang benar

Form upload wajib memakai enctype="multipart/form-data". Tanpa itu, PHP hanya menerima nama file tanpa isinya.

<form action="upload.php" method="post" enctype="multipart/form-data">
  <input type="file" name="dokumen" accept=".jpg,.jpeg,.png,.pdf">
  <button type="submit">Unggah</button>
</form>

Atribut accept membantu pengguna memilih file yang tepat, tetapi jangan mengandalkannya untuk validasi. Pemeriksaan tetap harus dilakukan di sisi server.

Memeriksa error bawaan dan ukuran file

Sebelum menyentuh nama atau tipe file, cek dulu $_FILES['dokumen']['error']. Kode error yang umum muncul adalah UPLOAD_ERR_INI_SIZE ketika file melebihi upload_max_filesize di php.ini. Pengguna perlu diberi tahu batas yang berlaku, bukan pesan kosong.

$file = $_FILES['dokumen'] ?? null;
if (!$file || $file['error'] !== UPLOAD_ERR_OK) {
    exit('File gagal diunggah. Kode: ' . ($file['error'] ?? 'tidak ada file'));
}

$maksimal = 2 * 1024 * 1024; // 2 MB
if ($file['size'] > $maksimal) {
    exit('Ukuran file melebihi 2 MB.');
}

Nilai size berasal dari klien, tetapi PHP sudah membandingkannya dengan batas server. Untuk keperluan validasi ukuran, angka ini cukup dapat diandalkan.

Memvalidasi tipe file dari isinya

Ekstensi dan MIME type yang dikirim browser mudah dipalsukan. Pakai finfo_file untuk membaca tipe file sebenarnya.

$izin = [
    'image/jpeg' => 'jpg',
    'image/png'  => 'png',
    'application/pdf' => 'pdf',
];

$finfo = new finfo(FILEINFO_MIME_TYPE);
$tipe = $finfo->file($file['tmp_name']);

if (!isset($izin[$tipe])) {
    exit('Tipe file tidak diizinkan.');
}

Daftar putih ini sekaligus menentukan ekstensi akhir file. Jangan memakai ekstensi dari $_FILES['name'] karena bisa berisi .php atau karakter aneh.

Membuat nama file baru dan memindahkannya

Nama asli file berpotensi mengandung spasi, garis miring, atau karakter non-ASCII. Ganti dengan nama acak yang aman, lalu pindahkan dari folder sementara.

$namaBaru = bin2hex(random_bytes(16)) . '.' . $izin[$tipe];
$tujuan = __DIR__ . '/../storage/uploads/' . $namaBaru;

if (!move_uploaded_file($file['tmp_name'], $tujuan)) {
    exit('Gagal menyimpan file.');
}

move_uploaded_file memastikan file benar-benar berasal dari upload, bukan path lokal yang disisipkan. Fungsi ini juga menghapus file sementara secara otomatis jika berhasil.

Menangani error dan memberi umpan balik

Setiap kegagalan sebaiknya mengembalikan pesan yang bisa ditindaklanjuti. Hindari menampilkan path server atau pesan $_FILES mentah karena bisa membocorkan struktur direktori.

Jika upload melibatkan banyak field dan validasi lain, pola penanganan error yang sama bisa dipakai seperti pada panduan menampilkan pesan error validasi PHP di form. Simpan pesan di array, tampilkan di halaman form, dan jangan hentikan proses dengan exit di tengah alur yang punya banyak field.

Menyajikan file yang sudah diunggah

Karena file disimpan di luar public_html, tautan langsung tidak tersedia. Buat skrip perantara yang memeriksa hak akses, lalu mengirim file dengan header yang sesuai.

$path = __DIR__ . '/../storage/uploads/' . basename($_GET['file']);
if (!is_file($path)) {
    http_response_code(404);
    exit;
}

header('Content-Type: ' . mime_content_type($path));
header('Content-Length: ' . filesize($path));
readfile($path);

basename mencegah path traversal seperti ../../etc/passwd. Jika file bersifat privat, tambahkan pemeriksaan sesi sebelum readfile dijalankan.

Checklist keamanan upload file

  • Simpan file di luar folder publik atau blokir eksekusi di folder upload.
  • Batasi ukuran lewat upload_max_filesize dan post_max_size di php.ini.
  • Validasi tipe file dengan finfo, bukan hanya ekstensi.
  • Ganti nama file dengan string acak dan ekstensi dari daftar putih.
  • Gunakan move_uploaded_file, bukan copy atau rename.
  • Jangan tampilkan pesan error server ke pengguna.

Jika setelah upload halaman malah menampilkan error 500, penyebabnya sering ada pada izin folder atau konfigurasi PHP. Langkah pemeriksaannya ada di panduan menangani error 500 di PHP.

Ringkasan

Upload file yang aman bertumpu pada tiga hal: simpan di lokasi yang tidak bisa dieksekusi, validasi tipe dari isi file, dan ganti nama file sebelum dipindahkan. Sisanya adalah penanganan error yang jelas agar pengguna tahu apa yang harus diperbaiki.

Pertanyaan yang Sering Diajukan

Apakah cukup memeriksa ekstensi file di JavaScript?

Tidak. Pemeriksaan di browser hanya untuk kenyamanan. Penyerang bisa mengirim request langsung ke server tanpa melewati JavaScript, jadi validasi tipe dan ukuran wajib diulang di PHP.

Mengapa file saya gagal diunggah padahal ukurannya kecil?

Periksa upload_max_filesize dan post_max_size di php.ini. Jika form berisi banyak field, total ukuran POST bisa melewati post_max_size sehingga $_FILES kosong.

Bisakah file disimpan di folder yang sama dengan skrip PHP?

Bisa, tetapi berisiko. Jika folder itu dapat diakses lewat URL dan server mengeksekusi PHP, file berbahaya bisa berjalan. Blokir eksekusi lewat .htaccess atau simpan file di luar public_html.