1. Executive Summary & File Upload Attack Vectors
Unrestricted file uploads represent one of the most critical vulnerabilities in web applications (CWE-434). If an attacker successfully uploads an executable script (such as a PHP web shell, JSP file, or malicious SVG with embedded JavaScript) to a web-accessible directory, they can achieve instantaneous Remote Code Execution (RCE) and complete server compromise.
CWE Reference
CWE-434 (Unrestricted Upload of File with Dangerous Type) • OWASP Top 10 A04/A05 • CERT-In Critical Vulnerability
2. The 6-Stage Secure File Upload Pipeline
<?php
/**
* Hardened File Upload Handler
*/
function handleSecureUpload(array $file): array {
$maxFileSize = 5 * 1024 * 1024; // 5 MB Strict Limit
$allowedExtensions = ['pdf' => 'application/pdf', 'png' => 'image/png', 'jpg' => 'image/jpeg'];
// 1. Validate Upload Error Code
if (!isset($file['error']) || is_array($file['error']) || $file['error'] !== UPLOAD_ERR_OK) {
throw new RuntimeException('Upload failed with server error code: ' . ($file['error'] ?? 'unknown'));
}
// 2. Validate File Size
if ($file['size'] > $maxFileSize) {
throw new RuntimeException('File size exceeds 5MB limit.');
}
// 3. Validate Extension via Strict Allow-List
$originalName = $file['name'];
$extension = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
if (!array_key_exists($extension, $allowedExtensions)) {
throw new RuntimeException('Disallowed file extension.');
}
// 4. Validate Content via Magic Bytes (Never trust Content-Type header!)
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($file['tmp_name']);
if ($mimeType !== $allowedExtensions[$extension]) {
throw new RuntimeException('File content does not match reported extension.');
}
// 5. Generate Unpredictable Random Storage Name (UUID v4)
$storageDir = '/var/www/webotg/storage/uploads'; // OUTSIDE web root!
$randomName = bin2hex(random_bytes(16)) . '.' . $extension;
$targetPath = $storageDir . '/' . $randomName;
// 6. Move from Temporary Directory
if (!move_uploaded_file($file['tmp_name'], $targetPath)) {
throw new RuntimeException('Failed to persist uploaded artifact.');
}
return [
'original_name' => htmlspecialchars($originalName, ENT_QUOTES, 'UTF-8'),
'stored_name' => $randomName,
'size_bytes' => $file['size'],
'mime_type' => $mimeType
];
}
?>
3. Web Server Directory Hardening (Nginx & Apache)
Even if an adversary manages to bypass validation, the storage directory must be physically incapable of executing scripts:
A. Nginx: Disabling Execution in Uploads Directory
# Block execution of all scripts in uploads location
location ^~ /uploads/ {
# Deny direct execution of PHP, Python, CGI, or Perl scripts
location ~ \.(php|phtml|php5|py|pl|cgi|sh|exe)$ {
deny all;
return 404;
}
}
B. Safe File Download Streaming
Never expose direct links to uploaded files. Stream them through a controller with defensive headers:
// Force browser to download rather than execute inline
header('Content-Type: ' . $mimeType);
header('Content-Disposition: attachment; filename="' . $safeFilename . '"');
header('X-Content-Type-Options: nosniff');
header('Content-Length: ' . filesize($filePath));
readfile($filePath);
exit;
4. File Upload Security Checklist
| Audit Check |
Defense Mechanism |
Pass Criteria |
| Storage Location |
Files stored in private directory outside web document root. |
Direct URL traversal returns 404. |
| Filename Randomization |
Original filename stripped; randomized UUID assigned. |
Zero path traversal (../) or double-extension attacks. |
| MIME Magic Inspection |
Binary signature verified via finfo inspection. |
Executable scripts masked as .jpg immediately dropped. |
| Execution Prevention |
Web server blocks PHP/CGI fastcgi handler in storage folder. |
Script cannot execute even if written to disk. |
Advertisement
WE
WebOTG Standards Directorate
Principal AppSec Auditor
WebOTG provides benchmark reference documentation, automated matrix evaluators, and security test harnesses for government digital platforms, WQMS architectures, and STQC compliance frameworks.