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:
124
functions.php
124
functions.php
@@ -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 ====================
|
||||
|
||||
Reference in New Issue
Block a user