Laravel File Upload Security: How to Validate Type, Size, and Storage Location
File uploads without type or size validation let attackers upload PHP shells, oversized files, or executable scripts that compromise your server.
The Problem
Unrestricted file uploads are a critical attack vector. Without proper validation, an attacker can upload a PHP file disguised as an image, access it via the web server, and execute arbitrary code on your server. Even if the file is not directly executable, oversized uploads can cause denial of service, and files with double extensions (image.php.jpg) can bypass naive extension checks. Laravel provides strong validation tools, but they must be applied explicitly.
How to Fix
-
1
Validate file type using mimes and mimetypes rules
Always validate the actual file type, not just the extension:
$request->validate([ 'avatar' => 'required|file|mimes:jpg,jpeg,png,webp|max:2048', 'document' => 'required|file|mimetypes:application/pdf|max:10240', ]);The mimes rule checks the file extension. The mimetypes rule checks the actual MIME type by reading the file content. Use both for maximum safety:
'photo' => 'required|file|mimes:jpg,png|mimetypes:image/jpeg,image/png|max:5120', -
2
Set a maximum file size
Always enforce a size limit to prevent denial of service:
// In validation rules (size in kilobytes) 'file' => 'required|file|max:10240', // 10 MB max// Also set PHP limits in php.ini upload_max_filesize = 10M post_max_size = 12MSet limits appropriate to your use case. Profile photos need 2 MB at most; documents might need 10 MB. -
3
Store uploads outside the public directory
Never store uploads directly in public/. Use Laravel's storage system:
// Store in storage/app/uploads (not publicly accessible) $path = $request->file('document')->store('uploads');// If you need public access, use the public disk with a unique name $path = $request->file('avatar')->store('avatars', 'public');// Generate a unique filename to prevent overwriting $path = $request->file('avatar')->storeAs( 'avatars', Str::uuid() . '.' . $request->file('avatar')->extension(), 'public' ); -
4
Disable PHP execution in upload directories
Even with validation, add a defense-in-depth layer by disabling PHP execution in upload directories.
Nginx: location ~* /storage/.*\.php$ { deny all; return 404; }
Apache (.htaccess in storage/app/public/): php_flag engine off <FilesMatch "\.php$"> Order allow,deny Deny from all </FilesMatch>
How to Verify
Test by attempting to upload a PHP file:
# Create a test PHP file
echo '<?php phpinfo();' > test.php.jpg
# Try uploading via your form — it should be rejected
Also verify stored files are not directly executable by visiting their URL. Run php artisan stackshield:scan --check=SS009 to verify.
Prevention
Add file validation rules to every upload endpoint. Use a Form Request class for upload validation. Configure your web server to block PHP execution in storage directories. Consider using a CDN or object storage (S3) for user uploads to completely separate them from your application server.
Frequently Asked Questions
Can I trust the file extension to determine the file type?
No. An attacker can rename malicious.php to malicious.jpg. The mimetypes validation rule reads the file's actual content to determine its type, which is more reliable. Use both mimes (extension) and mimetypes (content) rules together.
Is it safe to store uploads in storage/app/public?
Yes, as long as you disable PHP execution in that directory and validate file types properly. Files in storage/app/public are served through the storage symlink (php artisan storage:link) and are accessible via URL, so they must be safe file types.
Related Security Terms
Related Guides
How to Fix an Exposed Laravel Storage Directory
Your Laravel storage directory is publicly accessible, exposing logs, cache files, and uploaded data. Learn how to restrict access.
Laravel Dangerous Function Calls: How to Eliminate eval, shell_exec, and system from Your Codebase
Functions like eval(), shell_exec(), system(), and proc_open() execute arbitrary code. If reachable from user input, they give attackers full server access.
Laravel Mass Assignment Vulnerability: How to Protect Eloquent Models with $fillable and $guarded
Eloquent models without $fillable or $guarded allow attackers to set any database column through request input, including is_admin, role, or email_verified_at.
Detect This Automatically with StackShield
StackShield continuously monitors your Laravel application from the outside and alerts you when security issues are found. No installation required.
Start Free Trial