Table of Contents
▼- Persiapan: Konfigurasi Filesystem dan Storage di Laravel
- Membuat Form Upload dengan Validasi MIME Type dan Ukuran File
- Menyimpan File dengan Nama Unik dan Struktur Folder Rapi
- Menampilkan Gambar dan Menangani Error Upload dengan Benar
- Tips Keamanan: Mencegah Upload File Berbahaya di Aplikasi Laravel
- Checklist Sebelum Deploy ke Production
Upload gambar di Laravel itu mudah kalau kamu hanya mau "bisa jalan".
Tapi kalau mau aman, rapi, dan tahan serangan? Ceritanya berbeda.
Banyak tutorial Laravel yang mengajarkan $request->file('image')->store('uploads') lalu selesai. Padahal tanpa validasi yang benar, fitur upload bisa jadi celah masuknya file berbahaya ke server kamu.
Artikel ini akan memandu kamu dari nol — mulai konfigurasi filesystem, membuat form upload, validasi server-side, menyimpan file dengan aman, hingga tips mencegah eksploitasi. Cocok untuk mahasiswa IT dan junior developer yang ingin belajar Laravel dengan benar.
Persiapan: Konfigurasi Filesystem dan Storage di Laravel
Sebelum menulis satu baris kode pun, pastikan konfigurasi filesystem Laravel kamu sudah benar.
Buka file config/filesystems.php. Di sinilah Laravel mendefinisikan disk penyimpanan.
// config/filesystems.php
'disks' => [
'local' => [
'driver' => 'local',
'root' => storage_path('app'),
],
'public' => [
'driver' => 'local',
'root' => storage_path('app/public'),
'url' => env('APP_URL') . '/storage',
'visibility' => 'public',
],
],
Untuk menyimpan gambar yang bisa diakses publik, gunakan disk public.
Setelah itu, jalankan perintah berikut untuk membuat symlink dari public/storage ke storage/app/public:
php artisan storage:link
Perintah ini wajib dijalankan sekali di setiap environment (local dan production). Tanpanya, gambar yang kamu simpan tidak akan bisa diakses lewat URL.
Pastikan juga file .env kamu memiliki nilai APP_URL yang benar:
APP_URL=http://localhost:8000
Membuat Form Upload dengan Validasi MIME Type dan Ukuran File
Ini bagian paling krusial yang sering diabaikan developer pemula.
Membuat Form HTML
Buat view sederhana untuk form upload. Perhatikan atribut enctype — ini wajib ada agar file bisa dikirim:
<!-- resources/views/upload/form.blade.php -->
<form action="{{ route('upload.store') }}" method="POST" enctype="multipart/form-data">
@csrf
<div>
<label for="image">Pilih Gambar</label>
<input
type="file"
name="image"
id="image"
accept="image/jpeg,image/png,image/webp"
>
@error('image')
<p style="color:red;">{{ $message }}</p>
@enderror
</div>
<button type="submit">Upload Gambar</button>
</form>
Atribut accept di sisi client itu hanyalah UX helper — bukan keamanan. Validasi sesungguhnya harus terjadi di server.
Validasi di Controller
Buat controller dengan validasi yang ketat:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class ImageUploadController extends Controller
{
public function store(Request $request)
{
// Validasi server-side yang ketat
$validated = $request->validate([
'image' => [
'required',
'file',
'image', // Memastikan file adalah gambar
'mimes:jpeg,jpg,png,webp', // Whitelist MIME type
'max:2048', // Maksimal 2MB (dalam kilobyte)
'dimensions:min_width=100,min_height=100,max_width=2000,max_height=2000',
],
]);
// Lanjut ke proses penyimpanan...
}
}
Penjelasan setiap rule validasi:
file— memastikan input benar-benar file, bukan string atau arrayimage— Laravel akan memeriksa apakah file adalah gambar valid menggunakan getimagesize()mimes:jpeg,jpg,png,webp— whitelist ekstensi file yang diizinkanmax:2048— batas ukuran file 2MBdimensions— membatasi resolusi gambar agar tidak kehabisan memori saat diproses
Gunakan pesan error kustom agar lebih informatif bagi pengguna:
$messages = [
'image.required' => 'Gambar wajib diunggah.',
'image.image' => 'File yang diunggah harus berupa gambar.',
'image.mimes' => 'Format gambar harus JPEG, PNG, atau WebP.',
'image.max' => 'Ukuran gambar maksimal 2MB.',
'image.dimensions' => 'Dimensi gambar tidak valid.',
];
$validated = $request->validate([
'image' => ['required', 'file', 'image', 'mimes:jpeg,jpg,png,webp', 'max:2048'],
], $messages);
Menyimpan File dengan Nama Unik dan Struktur Folder Rapi
Jangan pernah menyimpan file dengan nama asli dari user. Ini adalah kesalahan keamanan klasik.
Nama file asli bisa mengandung karakter berbahaya, path traversal (../../etc/passwd), atau menimpa file yang sudah ada.
Menggunakan Nama Unik dengan UUID
use Illuminate\Support\Str;
public function store(Request $request)
{
$request->validate([
'image' => ['required', 'file', 'image', 'mimes:jpeg,jpg,png,webp', 'max:2048'],
]);
$file = $request->file('image');
$extension = $file->getClientOriginalExtension();
// Generate nama unik dengan UUID
$filename = Str::uuid() . '.' . strtolower($extension);
// Struktur folder berdasarkan tahun/bulan
$folder = 'images/' . date('Y/m');
// Simpan ke disk public
$path = $file->storeAs($folder, $filename, 'public');
// Simpan path ke database
// ImageModel::create(['path' => $path, 'user_id' => auth()->id()]);
return redirect()->back()->with('success', 'Gambar berhasil diunggah.');
}
Struktur folder images/2026/07/ akan membuat penyimpanan tetap rapi meski file terus bertambah.
Alternatif: Menggunakan hashName()
Laravel menyediakan helper hashName() yang lebih ringkas:
$path = $request->file('image')->store('images/' . date('Y/m'), 'public');
// Hasilnya: images/2026/07/9f3a2b1c4d5e6f7a.jpg (nama hash otomatis)
Pilih antara UUID atau hashName sesuai kebutuhan — keduanya aman karena tidak menggunakan nama asli file.
Menampilkan Gambar dan Menangani Error Upload dengan Benar
Menampilkan Gambar dari Storage
Setelah path tersimpan di database, tampilkan gambar menggunakan helper Storage::url():
// Di Controller
use Illuminate\Support\Facades\Storage;
public function show($id)
{
$image = ImageModel::findOrFail($id);
$imageUrl = Storage::disk('public')->url($image->path);
return view('upload.show', compact('imageUrl'));
}
<!-- Di Blade View -->
<img
src="{{ Storage::disk('public')->url($image->path) }}"
alt="Deskripsi gambar"
loading="lazy"
>
Selalu gunakan Storage::url() daripada hardcode URL. Ini memastikan URL tetap benar di semua environment.
Menangani Kegagalan Upload
Bungkus proses penyimpanan dalam blok try-catch untuk menangani error secara graceful:
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Log;
public function store(Request $request)
{
$request->validate([
'image' => ['required', 'file', 'image', 'mimes:jpeg,jpg,png,webp', 'max:2048'],
]);
try {
$file = $request->file('image');
$filename = Str::uuid() . '.' . strtolower($file->getClientOriginalExtension());
$path = $file->storeAs('images/' . date('Y/m'), $filename, 'public');
if (!$path) {
throw new \Exception('Gagal menyimpan file ke storage.');
}
// Simpan ke database...
return redirect()->back()->with('success', 'Gambar berhasil diunggah.');
} catch (\Exception $e) {
Log::error('Upload gagal: ' . $e->getMessage());
return redirect()->back()->with('error', 'Upload gagal. Silakan coba lagi.');
}
}
Jangan tampilkan pesan error teknis ke pengguna. Log saja di server, tampilkan pesan generik yang ramah pengguna.
Menghapus File Lama Saat Update
Kalau user mengganti foto profil, jangan lupa hapus file lama:
public function update(Request $request, $id)
{
$request->validate([
'image' => ['nullable', 'file', 'image', 'mimes:jpeg,jpg,png,webp', 'max:2048'],
]);
$user = User::findOrFail($id);
if ($request->hasFile('image')) {
// Hapus file lama jika ada
if ($user->avatar && Storage::disk('public')->exists($user->avatar)) {
Storage::disk('public')->delete($user->avatar);
}
$filename = Str::uuid() . '.' . $request->file('image')->getClientOriginalExtension();
$path = $request->file('image')->storeAs('avatars', $filename, 'public');
$user->avatar = $path;
$user->save();
}
return redirect()->back()->with('success', 'Profil diperbarui.');
}
Tips Keamanan: Mencegah Upload File Berbahaya di Aplikasi Laravel
Validasi MIME type saja tidak cukup. Penyerang bisa memanipulasi header file untuk menyamarkan PHP script sebagai gambar.
1. Jangan Simpan File di Folder Public yang Bisa Dieksekusi
Simpan selalu file di storage/app/public, bukan langsung di public/. Folder public/storage hanya berisi symlink — file asli ada di luar webroot dan tidak bisa dieksekusi langsung oleh server.
2. Verifikasi Konten File dengan finfo
Tambahkan pengecekan MIME type menggunakan finfo untuk validasi yang lebih dalam:
use Illuminate\Validation\Rules\File;
$request->validate([
'image' => [
'required',
File::image()
->min(1) // Minimal 1KB
->max(2 * 1024) // Maksimal 2MB
->dimensions(Rule::dimensions()->maxWidth(2000)->maxHeight(2000)),
],
]);
Atau menggunakan pengecekan manual dengan finfo:
$file = $request->file('image');
$finfo = new \finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($file->getPathname());
$allowedMimes = ['image/jpeg', 'image/png', 'image/webp'];
if (!in_array($mimeType, $allowedMimes)) {
return back()->withErrors(['image' => 'Tipe file tidak diizinkan.']);
}
3. Rename File dan Hapus Metadata EXIF
Nama unik yang kamu buat sudah mengatasi path traversal. Tapi gambar juga bisa mengandung metadata EXIF yang menyimpan lokasi GPS atau informasi sensitif lainnya.
Gunakan library seperti Intervention Image untuk membersihkan EXIF saat menyimpan:
composer require intervention/image
use Intervention\Image\Laravel\Facades\Image;
$image = Image::read($request->file('image'));
$image->scaleDown(width: 1200); // Resize sekaligus
// Simpan tanpa metadata EXIF
$filename = Str::uuid() . '.jpg';
$path = storage_path('app/public/images/' . date('Y/m') . '/' . $filename);
$image->toJpeg(quality: 85)->save($path);
4. Batasi Upload Hanya untuk User Terautentikasi
Pastikan route upload dilindungi middleware auth:
// routes/web.php
Route::middleware(['auth'])->group(function () {
Route::get('/upload', [ImageUploadController::class, 'create'])->name('upload.create');
Route::post('/upload', [ImageUploadController::class, 'store'])->name('upload.store');
});
5. Rate Limiting untuk Upload
Tambahkan throttle agar tidak bisa di-spam:
Route::middleware(['auth', 'throttle:10,1'])->group(function () {
Route::post('/upload', [ImageUploadController::class, 'store'])->name('upload.store');
});
Artinya maksimal 10 request upload per menit per user.
Checklist Sebelum Deploy ke Production
Sebelum fitur upload kamu naik ke server production, pastikan semua poin ini sudah terpenuhi:
- Validasi
mimesdanimageaktif di server-side - Batas ukuran file sesuai kebutuhan bisnis
- File disimpan dengan nama UUID atau hash, bukan nama asli
-
php artisan storage:linksudah dijalankan di server - Folder penyimpanan di luar webroot atau non-executable
- Route upload dilindungi middleware
auth - Rate limiting aktif untuk mencegah spam upload
- Error handling dengan try-catch dan logging
- File lama dihapus saat user melakukan update
Fitur upload gambar yang benar bukan soal berapa baris kode yang kamu tulis — tapi soal berapa banyak skenario buruk yang kamu antisipasi sebelum terjadi.
Dengan validasi MIME type, nama file unik, dan penyimpanan di luar webroot, kamu sudah jauh lebih aman dari sebagian besar celah yang biasa dieksploitasi penyerang.
Kesulitan dengan tugas programming atau butuh bantuan coding? KerjaKode siap membantu menyelesaikan tugas IT dan teknik informatika kamu. Dapatkan bantuan profesional di jasa tugas IT KerjaKode.