Task 10: Harden file upload validation

Enhanced validateFileUpload() function in functions.php with comprehensive security:
- Hardcoded MIME type whitelist per file type (profile_picture, proof_of_payment, document)
- Strict file size limits per type (5MB images, 10MB documents)
- Extension validation against whitelist
- Double extension prevention (e.g., shell.php.jpg)
- MIME type verification using finfo
- Image validation with getimagesize()
- is_uploaded_file() verification
- Random filename generation to prevent path traversal

Updated file upload handlers:
- upload_profile_picture.php - Profile picture uploads (JPEG, PNG, GIF, WEBP, 5MB max)
- submit_pop.php - Proof of payment uploads (PDF only, 10MB max) + CSRF validation + audit logging
- add_campsite.php - Campsite thumbnail uploads + input validation + CSRF validation + audit logging

Security improvements:
- All uploads use random filenames to prevent directory traversal
- All uploads use secure file permissions (0644)
- File validation occurs before move_uploaded_file()
- Comprehensive error logging for failed uploads
- Audit logging for successful file operations
This commit is contained in:
twotalesanimation
2025-12-03 13:30:45 +02:00
parent 7b1c20410c
commit b120415d53
4 changed files with 279 additions and 140 deletions

View File

@@ -2280,40 +2280,134 @@ function sanitizeTextInput($text, $maxLength = 1000) {
* @param int $maxSize Maximum file size in bytes
* @return string|false Sanitized filename or false if invalid
*/
function validateFileUpload($file, $allowedTypes = [], $maxSize = 5242880) { // 5MB default
// Check for upload errors
if (!isset($file['error']) || $file['error'] !== UPLOAD_ERR_OK) {
/**
* HARDENED FILE UPLOAD VALIDATION
*
* Validates file uploads with strict security checks:
* - Verifies upload completion
* - Enforces strict MIME type validation using finfo
* - Enforces strict file size limits per type
* - Validates extensions against whitelist
* - Prevents double extensions (e.g., .php.jpg)
* - Generates random filenames to prevent path traversal
* - Validates actual file content matches extension
*
* @param array $file The $_FILES['fieldname'] array
* @param string $fileType The type of file being uploaded (profile_picture, proof_of_payment, document)
* @return array|false Returns ['filename' => randomName, 'extension' => ext] on success, false on failure
*/
function validateFileUpload($file, $fileType = 'document') {
// ===== CONFIGURATION: HARDCODED TYPE WHITELIST =====
$fileTypeConfig = [
'profile_picture' => [
'extensions' => ['jpg', 'jpeg', 'png', 'gif', 'webp'],
'mimeTypes' => ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
'maxSize' => 5242880, // 5MB
'description' => 'Profile Picture'
],
'proof_of_payment' => [
'extensions' => ['pdf'],
'mimeTypes' => ['application/pdf'],
'maxSize' => 10485760, // 10MB
'description' => 'Proof of Payment'
],
'document' => [
'extensions' => ['pdf', 'doc', 'docx'],
'mimeTypes' => ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
'maxSize' => 10485760, // 10MB
'description' => 'Document'
]
];
// Validate fileType exists in config
if (!isset($fileTypeConfig[$fileType])) {
error_log("Invalid file type requested: $fileType");
return false;
}
// Check file size
if (!isset($file['size']) || $file['size'] > $maxSize) {
$config = $fileTypeConfig[$fileType];
// ===== CHECK 1: Upload Error =====
if (!isset($file['error']) || !isset($file['tmp_name']) || !isset($file['size']) || !isset($file['name'])) {
error_log("File upload validation: Missing required file array keys");
return false;
}
// Check MIME type
if ($file['error'] !== UPLOAD_ERR_OK) {
error_log("File upload error code: {$file['error']} for type: $fileType");
return false;
}
// ===== CHECK 2: File Size Limit =====
if ($file['size'] > $config['maxSize']) {
error_log("File size {$file['size']} exceeds limit {$config['maxSize']} for type: $fileType");
return false;
}
if ($file['size'] <= 0) {
error_log("File size is zero or negative for type: $fileType");
return false;
}
// ===== CHECK 3: Extension Validation (Case-Insensitive) =====
$pathinfo = pathinfo($file['name']);
$extension = strtolower($pathinfo['extension'] ?? '');
if (empty($extension) || !in_array($extension, $config['extensions'], true)) {
error_log("Invalid extension '$extension' for type: $fileType. Allowed: " . implode(', ', $config['extensions']));
return false;
}
// ===== CHECK 4: Prevent Double Extensions (e.g., shell.php.jpg) =====
if (isset($pathinfo['filename'])) {
// Check if filename contains suspicious extensions
$suspiciousPatterns = ['/\.php/', '/\.phtml/', '/\.phar/', '/\.sh/', '/\.bat/', '/\.exe/', '/\.com/'];
foreach ($suspiciousPatterns as $pattern) {
if (preg_match($pattern, $pathinfo['filename'], $matches)) {
error_log("Suspicious pattern detected in filename: {$pathinfo['filename']} for type: $fileType");
return false;
}
}
}
// ===== CHECK 5: MIME Type Validation =====
$finfo = finfo_open(FILEINFO_MIME_TYPE);
if ($finfo === false) {
error_log("Failed to open fileinfo resource");
return false;
}
$mimeType = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
if (!in_array($mimeType, $allowedTypes, true)) {
if (!in_array($mimeType, $config['mimeTypes'], true)) {
error_log("Invalid MIME type '$mimeType' for type: $fileType. Expected: " . implode(', ', $config['mimeTypes']));
return false;
}
// Generate random filename with original extension
$pathinfo = pathinfo($file['name']);
$extension = strtolower($pathinfo['extension']);
// ===== CHECK 6: Additional Image Validation (for images) =====
if (in_array($fileType, ['profile_picture'])) {
$imageInfo = @getimagesize($file['tmp_name']);
if ($imageInfo === false) {
error_log("File is not a valid image for type: $fileType");
return false;
}
}
// Whitelist allowed extensions
$allowedExtensions = ['jpg', 'jpeg', 'png', 'pdf', 'gif', 'webp'];
if (!in_array($extension, $allowedExtensions)) {
// ===== CHECK 7: Verify File is Actually Uploaded (Not Executed) =====
if (!is_uploaded_file($file['tmp_name'])) {
error_log("File is not a valid uploaded file for type: $fileType");
return false;
}
// Generate random filename
// ===== GENERATE RANDOM FILENAME =====
$randomName = bin2hex(random_bytes(16)) . '.' . $extension;
return $randomName;
return [
'filename' => $randomName,
'extension' => $extension,
'mimeType' => $mimeType
];
}
// ==================== RATE LIMITING & ACCOUNT LOCKOUT FUNCTIONS ====================