Securing File Uploads

Comprehensive guide to validating, sanitising, and safely storing user-uploaded files in Multi Host deployments.

Updated September 2025

File upload functionality is essential for image hosting but represents a significant attack surface. This guide covers practical techniques for accepting uploads safely, from initial validation through secure storage.

Understanding Upload Risks

Before implementing defences, understand what attackers attempt:

Code execution attacks upload files that execute as server-side code (PHP, ASP, JSP). Success gives attackers control of your server.

Client-side attacks upload files that execute malicious code in browsers—HTML with JavaScript, SVG with embedded scripts, or images with XSS payloads.

Resource exhaustion overwhelms your server with extremely large files, many simultaneous uploads, or files that cause expensive processing.

Content-based attacks exploit vulnerabilities in image processing libraries through malformed files.

Policy violations upload illegal, abusive, or unwanted content that creates liability or harms users.

Each risk requires specific countermeasures layered together for effective protection.

Multi-Layer Validation

Effective upload security validates at multiple levels, assuming any single check might be bypassed.

Layer 1: File Extension Check

Check the filename extension first—the quickest filter:

$allowed_extensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
$extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));

if (!in_array($extension, $allowed_extensions)) {
    reject('File type not allowed');
}

Extension checking alone is insufficient—attackers easily rename files—but it catches casual mistakes and reduces processing load.

Layer 2: MIME Type Verification

Check the Content-Type header sent by the browser:

$allowed_mimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
$reported_mime = $_FILES['upload']['type'];

if (!in_array($reported_mime, $allowed_mimes)) {
    reject('Invalid file type');
}

Like extensions, MIME headers are user-controlled and unreliable alone. They're another layer in the defence.

Layer 3: Content Analysis

Examine actual file content using the fileinfo extension:

$finfo = new finfo(FILEINFO_MIME_TYPE);
$detected_mime = $finfo->file($temp_path);

if (!in_array($detected_mime, $allowed_mimes)) {
    reject('File content does not match allowed types');
}

This reads file signatures (magic bytes) to determine type regardless of extension or headers. Much harder to spoof, though sophisticated attacks can still succeed.

Layer 4: Image Parsing

For image uploads, verify the file actually parses as an image:

$image_info = getimagesize($temp_path);

if ($image_info === false) {
    reject('File is not a valid image');
}

// Verify dimensions are reasonable
if ($image_info[0] > 10000 || $image_info[1] > 10000) {
    reject('Image dimensions exceed limits');
}

If getimagesize() fails, the file isn't a properly formed image. This catches many malformed files and embedded content attempts.

Layer 5: Deep Content Inspection

For high-security deployments, use image processing libraries to fully parse and re-encode images:

// Using GD
$source = imagecreatefromstring(file_get_contents($temp_path));
if ($source === false) {
    reject('Image processing failed');
}

// Re-encode to strip any embedded content
imagejpeg($source, $destination, 85);
imagedestroy($source);

Re-encoding creates a new image file containing only valid image data. Any embedded scripts, appended data, or malformed structures are eliminated.

This approach adds processing overhead but provides the strongest protection against image-based attacks.

Safe File Storage

How and where you store uploads significantly affects security.

Use Non-Guessable Filenames

Replace user-provided filenames with random identifiers:

$new_filename = bin2hex(random_bytes(16)) . '.jpg';

Random filenames prevent:

  • Path traversal: ../../../etc/passwd becomes meaningless
  • Enumeration: Attackers can't guess valid URLs
  • Overwrites: Collisions become statistically impossible

Preserve original filenames in the database if needed for display, but never use them in filesystem paths.

Store Outside Document Root

Place uploads where the web server cannot directly execute them:

/var/www/html/          # Document root
/var/storage/uploads/   # Upload storage (outside document root)

Serve files through a PHP script that streams content with appropriate headers:

$file = getFileFromDatabase($id);
if (!$file || !userCanAccess($file)) {
    http_response_code(404);
    exit;
}

header('Content-Type: ' . $file['mime']);
header('Content-Disposition: inline; filename="' . $file['display_name'] . '"');
header('X-Content-Type-Options: nosniff');
readfile($file['path']);

This approach enables access control, logging, and ensures proper Content-Type headers.

Configure Web Server Correctly

If serving uploads directly, configure the web server to prevent execution:

Apache .htaccess in upload directory:

# Disable PHP execution
php_flag engine off

# Deny access to dangerous extensions
<FilesMatch "\.(php|phtml|php3|php4|php5|phar|phps)$">
    Require all denied
</FilesMatch>

# Force content types for safety
<FilesMatch "\.(?:jpe?g)$">
    ForceType image/jpeg
</FilesMatch>
<FilesMatch "\.png$">
    ForceType image/png
</FilesMatch>

Nginx configuration:

location /uploads {
    # Disable PHP processing
    location ~ \.php$ {
        deny all;
    }
    
    # Serve only allowed types
    location ~* \.(jpg|jpeg|png|gif|webp)$ {
        add_header X-Content-Type-Options nosniff;
    }
}

Set Restrictive Permissions

Upload directories need write access for the web server but should be as restrictive as possible:

chmod 750 /var/storage/uploads
chown www-data:www-data /var/storage/uploads

Avoid 777 permissions even when troubleshooting—they're rarely the solution and always a risk.

Handling Dangerous Content

OWASP guidance on unrestricted file uploads provides comprehensive coverage of upload vulnerabilities and mitigations. Key additional considerations:

Strip Image Metadata

EXIF data can contain location information, embedded thumbnails with malicious content, or oversized data designed to exploit parsers:

$config['strip_metadata'] = true;

Re-encoding images (as described above) removes metadata automatically. For workflows that preserve originals, use dedicated metadata stripping:

exiftool -all= image.jpg

Handle SVG Carefully

SVG files are XML that can contain JavaScript. If you must accept SVG:

  1. Parse and validate the XML structure
  2. Strip all <script> tags and event handlers
  3. Remove external references
  4. Consider converting to PNG for display
  5. Serve with Content-Security-Policy headers

Many services simply reject SVG uploads as the safest option.

Scan for Malware

For additional protection, integrate malware scanning:

$config['malware_scan'] = true;
$config['clamav_socket'] = '/var/run/clamav/clamd.sock';

ClamAV and similar tools catch known malware signatures. They won't catch zero-day exploits but add another defensive layer.

Size and Rate Limits

Prevent resource exhaustion through appropriate limits:

File Size Limits

$config['max_upload_size'] = 10 * 1024 * 1024;  // 10 MB

Also configure PHP limits:

upload_max_filesize = 12M
post_max_size = 15M

PHP limits should slightly exceed application limits to ensure your error handling runs rather than PHP rejecting the request.

Dimension Limits

$config['max_image_width'] = 10000;
$config['max_image_height'] = 10000;
$config['max_megapixels'] = 50;

Very large images consume substantial memory during processing—a 10,000×10,000 pixel image requires ~400MB just for pixel data.

Rate Limits

Restrict upload frequency to prevent spam:

$config['uploads_per_minute'] = 10;
$config['uploads_per_hour'] = 100;
$config['uploads_per_day'] = 500;

See the Rate Limiting guide for implementation details.

Monitoring and Response

Logging upload activity enables detection and investigation:

log_upload([
    'user_id' => $user->id,
    'ip_address' => $request->ip(),
    'filename_original' => $uploaded_name,
    'filename_stored' => $stored_name,
    'file_size' => $file_size,
    'mime_type' => $detected_mime,
    'validation_result' => $result
]);

Monitor for patterns indicating attack or abuse:

  • High volume of rejected uploads
  • Unusual file types or sizes
  • Uploads from suspicious IP ranges
  • Repeated validation failures from same source

Automated alerting helps catch problems early.

Frequently Asked Questions

Extension validation alone is insufficient, but it's still valuable as the first layer. It catches mistakes, reduces processing load on invalid files, and combines with other layers for defence in depth.