Validasi upload tidak boleh berhenti pada ekstensi yang tertera di nama berkas. Nama foto.jpg bisa diklaim siapa pun dari browser mana pun, sementara isinya bisa jadi skrip PHP yang menunggu dieksekusi begitu berkas mendarat di folder yang bisa diakses web. Chapter ini membedah dua lapis pertahanan: lapisan validasi Laravel yang memeriksa MIME dari isi berkas, dan pemeriksaan manual di sisi storage untuk ukuran, dimensi gambar, serta penamaan yang aman.
Lapisan Pertama: Rule mimes dan max #
Rule mimes bekerja lebih keras daripada yang terlihat dari namanya. Laravel membaca isi berkas, menebak MIME type-nya, lalu membandingkan hasil tebakan itu dengan daftar ekstensi yang kita izinkan — nama berkas dari klien hanya jadi syarat kedua. Ditambah lagi, Laravel memblokir ekstensi php, phtml, phar, dan sekerabatnya bahkan sebelum pemeriksaan MIME dimulai:
$request->validate([
'dokumen' => ['required', 'file', 'mimes:pdf,png,jpg', 'max:2048'],
]);Detail lengkap rule file, mimes, dan max untuk upload sudah kita bahas di section Validasi dan Form
. Sekarang kita buktikan bahwa pemeriksaan itu sungguh membaca isi berkas.
MIME yang Diklaim vs Isi yang Sebenarnya #
Object UploadedFile menyimpan dua informasi MIME yang maknanya berbeda. getClientMimeType() adalah klaim yang dikirim browser pada header multipart — anggap ini kata pengguna, bukan bukti. guessExtension() bekerja sebaliknya: menebak dari isi berkas di server. Kita uji keduanya pada berkas yang berisi kode PHP tetapi mengaku sebagai JPEG:
use Symfony\Component\HttpFoundation\File\UploadedFile;
// berkas sungguhan berisi kode PHP, diklaim sebagai image/jpeg
$tmp = tempnam(sys_get_temp_dir(), 'dis').'.jpg';
file_put_contents($tmp, '<?php echo "halo"; ?>');
$file = new UploadedFile($tmp, 'foto.jpg', 'image/jpeg', null, true);
echo "mime yang diklaim klien : ".$file->getClientMimeType()."\n";
echo "ekstensi hasil tebakan : ".var_export($file->guessExtension(), true)."\n";
echo "mime hasil tebakan : ".var_export($file->getMimeType(), true)."\n";Output yang Dihasilkan:
mime yang diklaim klien : image/jpeg
ekstensi hasil tebakan : NULL
mime hasil tebakan : 'text/x-php'Klaim browser dan isi berkas bercerita dua hal yang berbeda: klien mengaku image/jpeg, tetapi isi berkas tercium sebagai text/x-php. Hasil guessExtension() yang NULL terjadi karena text/x-php tidak punya padanan ekstensi di daftar Symfony — dan justru nilai NULL inilah yang membuat rule mimes:pdf,png,jpg menolak berkas tersebut: tidak ada kecocokan dengan pdf, png, maupun jpg.
Membuktikan Penolakan lewat Feature Test #
Test berikut mengirim berkas PHP menyamar itu ke route upload sungguhan — test pertama dari empat test dalam kelas VerifikasiUploadTest. Berkas dibuat manual lewat tempnam() dan file_put_contents() — bukan lewat UploadedFile::fake() — dengan alasan penting yang kita bahas setelah ini:
public function test_berkas_php_yang_menyamar_jpg_ditolak(): void
{
Storage::fake('local');
$user = User::factory()->create();
// berkas sungguhan berisi kode PHP, diklaim sebagai image/jpeg
$tmp = tempnam(sys_get_temp_dir(), 'dis').'.jpg';
file_put_contents($tmp, '<?php echo "halo"; ?>');
$berkas = new \Symfony\Component\HttpFoundation\File\UploadedFile(
$tmp, 'foto.jpg', 'image/jpeg', null, true
);
$response = $this->actingAs($user)->post('/dokumen', [
'dokumen' => $berkas,
]);
$response->assertSessionHasErrors('dokumen');
$this->assertSame(0, Dokumen::count());
}Output yang Dihasilkan (potongan php artisan test):
✓ berkas php yang menyamar jpg ditolak 0.16s
✓ berkas php asli dengan ekstensi php ditolak juga 0.02s
✓ dimensi gambar bisa diperiksa sebelum disimpan 0.04s
✓ ukuran berkas bisa diperiksa 0.01s
Tests: 4 passed (11 assertions)Empat test tersebut membuktikan: berkas PHP yang menyamar sebagai JPG ditolak, berkas skrip.php dengan nama aslinya juga ditolak, dan pemeriksaan dimensi serta ukuran berjalan seperti diharapkan.
Memeriksa Ukuran dan Dimensi #
Dua pemeriksaan tambahan layak dilakukan sebelum penyimpanan. Method getSize() memberi ukuran berkas dalam byte — berguna untuk kebijakan kuota per pengguna:
$file = UploadedFile::fake()->createWithContent('laporan-2024.pdf', $isiPdf);
$this->assertSame(76, $file->getSize());
$this->assertTrue($file->getSize() <= 2048 * 1024);Untuk gambar, getimagesize() membaca header gambar dan mengembalikan lebar, tinggi, serta MIME hasil pembacaan isi berkas — tanpa perlu library tambahan:
$file = UploadedFile::fake()->image('poster.png', 1200, 630);
$info = getimagesize($file->getPathname());
$this->assertSame(1200, $info[0]);
$this->assertSame(630, $info[1]);
$this->assertSame('image/png', $info['mime']);UploadedFile::fake()->image() menggambar PNG sungguhan berukuran 1200×630, sehingga getimagesize() membaca persis dimensi itu. Pola ini berguna untuk kebijakan seperti “poster minimal 1200×630 piksel” atau menolak gambar raksasa 10000×10000 yang bisa menghabiskan memori saat diproses.
Nama Aman: Hash atau Slug #
Nama asli dari pengguna tidak pernah boleh dipakai langsung sebagai nama berkas penyimpanan — nama itu bisa mengandung ../ untuk path traversal, karakter aneh, atau bentrokan antar pengguna. Dua strategi aman yang saling melengkapi:
- Nama hash — hasil bawaan
store(). Nama acak membuat serangan traversal dan bentrok mustahil; nama asli cukup disimpan di kolomjuduluntuk keperluan tampilan. - Nama slug — bila nama terbaca benar-benar dibutuhkan di path, bersihkan dengan
Str::sluglalu satukan kembali ekstensinya:
use Illuminate\Support\Str;
$namaAsli = 'Laporan Final (Budi) 2024!!.pdf';
$slug = Str::slug(pathinfo($namaAsli, PATHINFO_FILENAME));
$aman = $slug.'.'.pathinfo($namaAsli, PATHINFO_EXTENSION);
echo "nama asli : {$namaAsli}\n";
echo "nama aman : {$aman}\n";Output yang Dihasilkan:
nama asli : Laporan Final (Budi) 2024!!.pdf
nama aman : laporan-final-budi-2024.pdfEkstensi tetap diambil dari pathinfo, bukan digabung ke dalam slug, agar Str::slug tidak mengubah pdf menjadi bagian nama. Dengan dua lapis verifikasi ini — isi berkas diperiksa validasi, nama dibersihkan sebelum dipakai — berkas yang mendarat di disk sudah terjamin bentuk dan namanya aman. Selanjutnya kita telusuri sisi penyimpanan: method-method Storage yang menulis, menyalin, dan memindahkan berkas, di dokumentasi Filesystem
dan chapter Menyimpan File.