August 23, 2026
Clean Up Malicious User Uploads in Laravel
We often let users upload files to our applications. Things like profile pictures, document attachments, and so on. But relying on what the…

By Muhammad Azeem (Full-Stack Developer)
2 min read
We often let users upload files to our applications. Things like profile pictures, document attachments, and so on. But relying on what the user's browser tells us about these files is a big security risk. What if someone uploads a malicious script disguised as a perfectly innocent image?
An attacker could embed harmful code inside a seemingly valid JPEG or PDF. If we store these files directly and our web server is configured to execute certain file types (even if subtly), or if other parts of our system process them, we could be in serious trouble. This is a common way for attackers to try and gain control of a system.
The Common Solution (And why it fails)
Many developers, myself included in the past, often rely solely on basic validation when handling file uploads. We check the file's extension (like .jpg, .png) and its MIME type (image/jpeg, image/png) using Laravel's validation rules. This seems reasonable on the surface, and it covers basic user mistakes.
The problem is that both the file extension and the MIME type are just metadata sent by the user's browser. An attacker can easily spoof these values. They can take a PHP script, name it malicious.jpg, set its MIME type to image/jpeg, and then upload it. If your server blindly saves this, you have a ticking time bomb.
The Better Way
The better approach is to not blindly trust the incoming file's metadata for content safety. For image uploads, the trick is to re-encode the image. When you load an image file into an image processing library and then save it again, the library typically strips out any non-image data, including potentially malicious scripts or embedded headers.
For non-image files, or situations where re-encoding isn't an option, you should always store user uploads outside your web root. Then, serve them through a dedicated controller that checks permissions and streams the file content, rather than allowing direct public URL access. This setup prevents the web server from accidentally executing a disguised script.
The Code
<?php
namespace App\\Http\\Controllers;
use Illuminate\\Http\\Request;
use Illuminate\\Support\\Facades\\Storage;
class UploadController extends Controller
{
public function store(Request $request)
{
// Simple validation, which is often not enough for true security
$request->validate([
'avatar' => 'required|image|mimes:jpeg,png,jpg,gif|max:2048', // MIME type and extension check
]);
$path = $request->file('avatar')->store('avatars'); // Stores directly in 'storage/app/avatars'
return back()->with('success', 'Avatar uploaded based on simple validation!');
}
}<?php
namespace App\\Http\\Controllers;
use Illuminate\\Http\\Request;
use Illuminate\\Support\\Facades\\Storage;
class UploadController extends Controller
{
public function store(Request $request)
{
// Simple validation, which is often not enough for true security
$request->validate([
'avatar' => 'required|image|mimes:jpeg,png,jpg,gif|max:2048', // MIME type and extension check
]);
$path = $request->file('avatar')->store('avatars'); // Stores directly in 'storage/app/avatars'
return back()->with('success', 'Avatar uploaded based on simple validation!');
}
}Here, we're using Laravel's image and mimes validation rules. While this is good for basic checks and filtering expected file types, it only validates the header and extension of the file. It doesn't inspect or sanitize the actual content, leaving a loophole for disguised malicious files.
<?php
namespace App\\Http\\Controllers;
use Illuminate\\Http\\Request;
use Illuminate\\Support\\Facades\\Storage;
use Intervention\\Image\\Facades\\Image; // Don't forget to install: composer require intervention/image
class UploadController extends Controller
{
public function storeSecure(Request $request)
{
// Still do basic validation; it's good practice for expected file type and size
$request->validate([
'avatar' => 'required|image|mimes:jpeg,png,jpg,gif|max:2048',
]);
$imageFile = $request->file('avatar');
$filename = uniqid() . '.' . $imageFile->getClientOriginalExtension();
// Use Intervention Image to process and re-save the image.
// This critical step strips out potentially malicious data embedded in the original file.
$processedImage = Image::make($imageFile)->encode($imageFile->getClientOriginalExtension(), 90);
// Store the processed image safely, outside the web root (e.g., in 'storage/app/public')
Storage::put('avatars/' . $filename, $processedImage->stream()->__toString());
return back()->with('success', 'Secured avatar uploaded!');
}
}<?php
namespace App\\Http\\Controllers;
use Illuminate\\Http\\Request;
use Illuminate\\Support\\Facades\\Storage;
use Intervention\\Image\\Facades\\Image; // Don't forget to install: composer require intervention/image
class UploadController extends Controller
{
public function storeSecure(Request $request)
{
// Still do basic validation; it's good practice for expected file type and size
$request->validate([
'avatar' => 'required|image|mimes:jpeg,png,jpg,gif|max:2048',
]);
$imageFile = $request->file('avatar');
$filename = uniqid() . '.' . $imageFile->getClientOriginalExtension();
// Use Intervention Image to process and re-save the image.
// This critical step strips out potentially malicious data embedded in the original file.
$processedImage = Image::make($imageFile)->encode($imageFile->getClientOriginalExtension(), 90);
// Store the processed image safely, outside the web root (e.g., in 'storage/app/public')
Storage::put('avatars/' . $filename, $processedImage->stream()->__toString());
return back()->with('success', 'Secured avatar uploaded!');
}
}Here, we still perform initial validation. But the critical step is using Intervention\\Image::make($imageFile)->encode(...). This loads the image into memory, processes its pixel data, and then saves it again in a clean format. Any non-image data, like embedded scripts, gets completely stripped out. We then store this safely generated file.
The Takeaway
Always process and re-validate user-uploaded file content, don't just trust metadata.