The issue is that unvalidated file uploads create serious security vulnerabilities. You can solve this by implementing multiple validation layers:
Table Of Contents
The Fix
<?php
// Complete file upload validation
function validateUploadedFile(array $file, array $config = []): array {
$errors = [];
// Default configuration
$config = array_merge([
'max_size' => 5 * 1024 * 1024, // 5MB
'allowed_types' => ['jpg', 'jpeg', 'png', 'gif', 'pdf'],
'allowed_mimes' => [
'image/jpeg', 'image/png', 'image/gif', 'application/pdf'
],
'max_filename_length' => 255,
'check_image_dimensions' => true,
'max_width' => 2000,
'max_height' => 2000
], $config);
// Check upload errors
if ($file['error'] !== UPLOAD_ERR_OK) {
$errors[] = getUploadErrorMessage($file['error']);
return ['valid' => false, 'errors' => $errors];
}
// Check file size
if ($file['size'] > $config['max_size']) {
$errors[] = 'File size exceeds maximum allowed size of ' .
formatBytes($config['max_size']);
}
// Check filename length
if (strlen($file['name']) > $config['max_filename_length']) {
$errors[] = 'Filename is too long';
}
// Validate file extension
$extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($extension, $config['allowed_types'])) {
$errors[] = 'File type not allowed. Allowed types: ' .
implode(', ', $config['allowed_types']);
}
// Validate MIME type
$mimeType = mime_content_type($file['tmp_name']);
if (!in_array($mimeType, $config['allowed_mimes'])) {
$errors[] = 'Invalid file format detected';
}
// Additional validation for images
if (in_array($extension, ['jpg', 'jpeg', 'png', 'gif']) &&
$config['check_image_dimensions']) {
$imageInfo = getimagesize($file['tmp_name']);
if ($imageInfo === false) {
$errors[] = 'Invalid image file';
} else {
[$width, $height] = $imageInfo;
if ($width > $config['max_width'] || $height > $config['max_height']) {
$errors[] = "Image dimensions too large. Max: {$config['max_width']}x{$config['max_height']}";
}
}
}
// Check for executable content (basic)
if (isExecutableFile($file['tmp_name'])) {
$errors[] = 'Executable files are not allowed';
}
return [
'valid' => empty($errors),
'errors' => $errors,
'file_info' => [
'original_name' => $file['name'],
'size' => $file['size'],
'type' => $mimeType,
'extension' => $extension
]
];
}
// Get human-readable upload error messages
function getUploadErrorMessage(int $errorCode): string {
return match($errorCode) {
UPLOAD_ERR_INI_SIZE => 'File exceeds upload_max_filesize directive',
UPLOAD_ERR_FORM_SIZE => 'File exceeds MAX_FILE_SIZE directive',
UPLOAD_ERR_PARTIAL => 'File was only partially uploaded',
UPLOAD_ERR_NO_FILE => 'No file was uploaded',
UPLOAD_ERR_NO_TMP_DIR => 'Missing temporary folder',
UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk',
UPLOAD_ERR_EXTENSION => 'File upload stopped by extension',
default => 'Unknown upload error'
};
}
// Basic executable file detection
function isExecutableFile(string $filePath): bool {
$handle = fopen($filePath, 'rb');
if (!$handle) return false;
$header = fread($handle, 512);
fclose($handle);
// Check for common executable signatures
$executableSignatures = [
"\x7fELF", // Linux/Unix executable
"MZ", // Windows executable
"\x00\x00\x01\x00", // Windows icon
"<?php", // PHP script
"#!/", // Shell script
"<script", // JavaScript
];
foreach ($executableSignatures as $signature) {
if (str_starts_with($header, $signature)) {
return true;
}
}
return false;
}
// Secure file upload handler
function handleSecureUpload(string $inputName, string $uploadDir, array $config = []): array {
if (!isset($_FILES[$inputName])) {
return ['success' => false, 'error' => 'No file uploaded'];
}
$file = $_FILES[$inputName];
// Validate the file
$validation = validateUploadedFile($file, $config);
if (!$validation['valid']) {
return [
'success' => false,
'errors' => $validation['errors']
];
}
// Create secure filename
$originalName = $file['name'];
$extension = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
$safeFilename = generateSecureFilename($originalName);
// Ensure upload directory exists
if (!is_dir($uploadDir)) {
if (!mkdir($uploadDir, 0755, true)) {
return ['success' => false, 'error' => 'Cannot create upload directory'];
}
}
$targetPath = $uploadDir . DIRECTORY_SEPARATOR . $safeFilename;
// Move uploaded file
if (!move_uploaded_file($file['tmp_name'], $targetPath)) {
return ['success' => false, 'error' => 'Failed to move uploaded file'];
}
// Set secure permissions
chmod($targetPath, 0644);
return [
'success' => true,
'file_info' => [
'original_name' => $originalName,
'saved_name' => $safeFilename,
'path' => $targetPath,
'size' => $file['size'],
'type' => $validation['file_info']['type']
]
];
}
// Generate secure filename
function generateSecureFilename(string $originalName): string {
$extension = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
$basename = pathinfo($originalName, PATHINFO_FILENAME);
// Sanitize basename
$basename = preg_replace('/[^a-zA-Z0-9._-]/', '_', $basename);
$basename = trim($basename, '._-');
if (empty($basename)) {
$basename = 'file';
}
// Add timestamp and random string for uniqueness
$timestamp = date('Y-m-d_H-i-s');
$random = bin2hex(random_bytes(4));
return $basename . '_' . $timestamp . '_' . $random . '.' . $extension;
}
// Format bytes for display
function formatBytes(int $bytes): string {
$units = ['B', 'KB', 'MB', 'GB'];
for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) {
$bytes /= 1024;
}
return round($bytes, 2) . ' ' . $units[$i];
}
// Multiple file upload handler
function handleMultipleUploads(string $inputName, string $uploadDir, array $config = []): array {
if (!isset($_FILES[$inputName])) {
return ['success' => false, 'error' => 'No files uploaded'];
}
$files = $_FILES[$inputName];
$results = ['success' => true, 'files' => [], 'errors' => []];
// Handle array of files
if (is_array($files['name'])) {
for ($i = 0; $i < count($files['name']); $i++) {
$file = [
'name' => $files['name'][$i],
'type' => $files['type'][$i],
'tmp_name' => $files['tmp_name'][$i],
'error' => $files['error'][$i],
'size' => $files['size'][$i]
];
$result = handleSecureUpload(['file' => $file], $uploadDir, $config);
if ($result['success']) {
$results['files'][] = $result['file_info'];
} else {
$results['errors'][] = [
'file' => $file['name'],
'errors' => $result['errors'] ?? [$result['error']]
];
$results['success'] = false;
}
}
}
return $results;
}
// Usage example
try {
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$config = [
'max_size' => 2 * 1024 * 1024, // 2MB
'allowed_types' => ['jpg', 'png', 'pdf'],
'allowed_mimes' => ['image/jpeg', 'image/png', 'application/pdf']
];
$result = handleSecureUpload('uploaded_file', 'uploads/', $config);
if ($result['success']) {
echo "File uploaded successfully!\n";
echo "Saved as: " . $result['file_info']['saved_name'] . "\n";
echo "Size: " . formatBytes($result['file_info']['size']) . "\n";
} else {
echo "Upload failed:\n";
foreach ($result['errors'] ?? [$result['error']] as $error) {
echo "- $error\n";
}
}
}
} catch (Exception $e) {
echo "Upload error: " . $e->getMessage() . "\n";
}
Key Points
Comprehensive upload validation protects against multiple attack vectors:
- File Type Validation: Check both extension and MIME type to prevent disguised files
- Size Limits: Prevent DoS attacks through large file uploads
- Content Inspection: Basic checks for executable content
- Secure Storage: Generate safe filenames and set proper permissions
Critical Validations:
- Upload error codes for proper error handling
- File size against configured limits
- Extension whitelist (never blacklist)
- MIME type verification
- Image dimension checks for images
- Filename sanitization for security
Security Best Practices:
- Store uploads outside web root when possible
- Generate unique, unpredictable filenames
- Set restrictive file permissions
- Validate file content, not just headers
- Log upload attempts for monitoring
Essential for any application accepting user file uploads to prevent security vulnerabilities and ensure system stability.
Share this article
Add Comment
No comments yet. Be the first to comment!