File Upload Security

Medium

Tests file upload endpoints for security vulnerabilities.

Estimated fix time: 30 minutes

What is File Upload Security?

Insecure file upload handling can allow attackers to upload malicious files, execute arbitrary code, or perform directory traversal attacks.

Security Impact

Severity: High to Critical

  • Remote code execution
  • Malware distribution
  • Defacement
  • Data exfiltration
  • Server compromise

How to Fix

1. Validate File Types

public function upload(Request $request)
{
    $request->validate([
        'file' => 'required|file|mimes:jpg,jpeg,png,pdf|max:2048',
    ]);
    
    // Process upload
}

2. Validate MIME Types (Server-Side)

use Illuminate\Support\Facades\Storage;
use Symfony\Component\Mime\MimeTypes;

public function upload(Request $request)
{
    $file = $request->file('file');
    
    // Get actual MIME type
    $mimeType = $file->getMimeType();
    $allowedMimes = ['image/jpeg', 'image/png', 'application/pdf'];
    
    if (!in_array($mimeType, $allowedMimes)) {
        return back()->withErrors(['file' => 'Invalid file type']);
    }
    
    // Store file
    $path = $file->store('uploads', 'private');
}

3. Generate Unique Filenames

use Illuminate\Support\Str;

$filename = Str::uuid() . '.' . $file->getClientOriginalExtension();
$file->storeAs('uploads', $filename, 'private');

4. Store Files Outside Web Root

// config/filesystems.php
'disks' => [
    'uploads' => [
        'driver' => 'local',
        'root' => storage_path('app/uploads'),
        'visibility' => 'private',
    ],
],

// Store
$path = $file->store('', 'uploads');

// Download (through controller)
public function download($filename)
{
    return Storage::disk('uploads')->download($filename);
}

5. Scan for Malware

// Using ClamAV
use Xenolope\Quahog\Client;

public function scanFile($filePath)
{
    $quahog = new Client('unix:///var/run/clamav/clamd.ctl');
    $result = $quahog->scanLocalFile($filePath);
    
    if ($result['status'] === 'FOUND') {
        unlink($filePath);
        throw new \Exception('Malware detected');
    }
}

6. Implement File Size Limits

// config/upload.php
'max_file_size' => 2048, // KB

// Validation
$request->validate([
    'file' => 'required|file|max:' . config('upload.max_file_size'),
]);

7. Prevent Double Extensions

$allowedExtensions = ['jpg', 'png', 'pdf'];
$extension = strtolower($file->getClientOriginalExtension());

if (!in_array($extension, $allowedExtensions)) {
    return back()->withErrors(['file' => 'Invalid file extension']);
}

8. Add Image Processing

use Intervention\Image\Facades\Image;

// Re-encode images to strip metadata and potential exploits
public function processImage($file)
{
    $img = Image::make($file);
    $img->encode('jpg', 85);
    
    $filename = Str::uuid() . '.jpg';
    Storage::disk('uploads')->put($filename, (string) $img);
    
    return $filename;
}

Complete Example

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Storage;

class FileUploadController extends Controller
{
    public function upload(Request $request)
    {
        // Validate
        $request->validate([
            'file' => [
                'required',
                'file',
                'mimes:jpg,jpeg,png,pdf',
                'max:2048',
                function ($attribute, $value, $fail) {
                    // Additional MIME check
                    $mimeType = $value->getMimeType();
                    $allowed = ['image/jpeg', 'image/png', 'application/pdf'];
                    
                    if (!in_array($mimeType, $allowed)) {
                        $fail('Invalid file type.');
                    }
                },
            ],
        ]);
        
        $file = $request->file('file');
        
        // Generate secure filename
        $filename = Str::uuid() . '.' . $file->getClientOriginalExtension();
        
        // Store in private disk
        $path = $file->storeAs('uploads', $filename, 'private');
        
        // Save to database
        auth()->user()->files()->create([
            'filename' => $filename,
            'original_name' => $file->getClientOriginalName(),
            'mime_type' => $file->getMimeType(),
            'size' => $file->getSize(),
            'path' => $path,
        ]);
        
        return back()->with('success', 'File uploaded successfully');
    }
    
    public function download($id)
    {
        $file = auth()->user()->files()->findOrFail($id);
        
        return Storage::disk('private')->download(
            $file->path,
            $file->original_name
        );
    }
}

Verification Steps

  1. Try uploading executable file - should be rejected
  2. Try uploading file with double extension - should be handled
  3. Try uploading oversized file - should be rejected
  4. Verify files stored outside web root
  5. Test direct file access - should be blocked

Best Practices

  • Always validate on server-side
  • Never trust client-supplied filenames
  • Store files outside web root
  • Use private file systems
  • Implement malware scanning
  • Re-encode images
  • Set file size limits
  • Log all uploads
  • Implement virus scanning
  • Use Content-Disposition headers
  • Cloud Storage Security
  • CSRF Protection
  • Directory Exposure

Automatically detect this issue

StackShield can automatically scan your Laravel application for this security issue and alert you when it's detected.

Start Free Trial
Was this guide helpful?