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.

High severity Application Security Updated 2026-05-01 Markdown

Unrestricted file uploads represent one of the most direct paths to server compromise. Web shells — small PHP files that accept commands through HTTP parameters — have been the initial access technique in some of the most damaging breaches. If an attacker can upload a .php file and access it through the web server, they have arbitrary code execution with the web server's privileges.

Attackers bypass naive extension-based validation through several techniques. The double-extension trick (shell.php.jpg) exploits misconfigured web servers that serve PHP based on the first extension. MIME spoofing involves prepending a valid image header to a PHP file so it passes content-type checks. Null byte injection (shell.php�.jpg) terminates the filename at the null byte in some PHP versions. These bypass techniques work against developers who check only file extensions or trust the browser-supplied Content-Type header.

File upload vulnerabilities appear in applications of all sizes. According to threat intelligence reports, web shell deployment is one of the most common initial access techniques used by ransomware groups. Defense requires multiple layers: validating file content (not just extension), storing uploads outside the web root, disabling PHP execution in upload directories, and considering re-encoding uploaded images to strip embedded payloads.

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. 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. 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 = 12M
    Set limits appropriate to your use case. Profile photos need 2 MB at most; documents might need 10 MB.
  3. 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. 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.

Should I re-process uploaded images before storing them?

Yes. Use Intervention Image or a similar library to re-encode uploaded images (Image::make($file)->encode("jpg")->save($path)). Re-encoding strips metadata, eliminates embedded payloads in image files, and normalizes the format. This is a strong additional defense even if the attacker bypasses type validation, since the re-encoded file will be a legitimate image.

Can SVG files be used for XSS attacks?

Yes. SVG files are XML-based and can contain JavaScript in <script> tags or event handlers like onload. Never serve SVG files uploaded by users as inline content or with a Content-Type: image/svg+xml header unless you sanitize them first. Either strip JavaScript from SVGs server-side or convert them to a raster format like PNG.

How do I securely serve private uploaded files to authorized users?

Route file access through an authenticated controller that checks authorization before reading from storage/app/private/. Use return response()->file($path) or return Storage::download($path) after verifying the user has permission. Never expose private file storage paths directly as public URLs — always mediate access through application logic.

Related Security Terms

Free security check

Is your Laravel app exposed right now?

34% of Laravel apps we scan have at least one critical issue. Most teams don't find out until something breaks. Our free scan checks your live application in under 60 seconds.

18% have debug mode on
72% missing security headers
12% have exposed .env
Scan My App Free No signup required. Results in 60 seconds.