Securing File Uploads
Comprehensive guide to validating, sanitising, and safely storing user-uploaded files in Multi Host deployments.
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/passwdbecomes 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:
- Parse and validate the XML structure
- Strip all
<script>tags and event handlers - Remove external references
- Consider converting to PNG for display
- Serve with
Content-Security-Policyheaders
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.