🏠 Home 👤 About ⚡ Skills 💼 Portfolio 📦 Packages 📝 Blog ✉ Contact ✉ Contact Now
All Articles

File Uploads in Laravel: Storage, Validation and Serving

Uploads look like a solved problem until you deploy to shared hosting without symlinks, or someone uploads a PHP file named avatar.png.

Junaid Ali 2 min read 0 views

File uploads combine three things that each go wrong independently: validation, storage location, and how the file gets served back. Here is how I handle each.

Validate Properly

$request->validate([
    'cover_image' => 'required|image|mimes:png,jpg,jpeg,webp|max:3072',
]);

The image rule checks the actual file contents, not the extension. That matters: shell.php renamed to avatar.png passes an extension check and fails a real MIME check.

Store Outside the Web Root

$path = $request->file('cover_image')->store('blog', 'public');
// storage/app/public/blog/xxxx.jpg

Laravel generates a random filename, which quietly solves two problems: collisions, and a user-controlled name ending up in a URL.

When storage:link Is Not Available

Plenty of shared hosts do not allow symlinks. Rather than storing uploads in public/ — which loses you the safety above — serve them through a controller:

Route::get('/jfile/{path}', function (string $path) {
    if (str_contains($path, '..')) abort(403);

    $full = storage_path('app/public/' . ltrim($path, '/'));
    abort_unless(file_exists($full) && !is_dir($full), 404);

    $mime = mime_content_type($full);
    abort_unless(in_array($mime, ['image/jpeg','image/png','image/webp','application/pdf']), 403);

    return response()->file($full, ['Cache-Control' => 'public, max-age=31536000']);
})->where('path', '.*');

Note the two guards: the traversal check, and the MIME allow-list. Without the second one, this route will happily serve any file that ends up in that directory.

Delete the Old File on Replace

if ($request->hasFile('cover_image')) {
    if ($post->cover_image) Storage::disk('public')->delete($post->cover_image);
    $data['cover_image'] = $request->file('cover_image')->store('blog', 'public');
}

Skip this and your disk fills with orphaned files nobody can identify a year later.

Watch Your PHP Limits

A Laravel rule of max:10240 means nothing if upload_max_filesize is 2M — PHP rejects the request before Laravel sees it, and the user gets a confusing empty error. Check upload_max_filesize, post_max_size and max_execution_time together.

Tagged Laravel File Uploads Security Storage
Share this article

Want this built properly?

I take on a small number of projects at a time so each one gets real attention.

WhatsApp Teams LinkedIn Facebook GitHub