diff --git a/add_campsite.php b/add_campsite.php index 6d2ca28c..6d6554b9 100644 --- a/add_campsite.php +++ b/add_campsite.php @@ -2,7 +2,7 @@ include_once('functions.php'); require_once("env.php"); session_start(); -$user_id = $_SESSION['user_id']; // assuming you're storing it like this +$user_id = $_SESSION['user_id'] ?? null; // CSRF Token Validation if (!isset($_POST['csrf_token']) || !validateCSRFToken($_POST['csrf_token'])) { @@ -14,26 +14,48 @@ if (!isset($_POST['csrf_token']) || !validateCSRFToken($_POST['csrf_token'])) { $conn = openDatabaseConnection(); // Get text inputs -$name = $_POST['name']; -$desc = $_POST['description']; -$lat = $_POST['latitude']; -$lng = $_POST['longitude']; -$website = $_POST['website']; -$telephone = $_POST['telephone']; +$name = validateName($_POST['name'] ?? '') ?: ''; +$desc = isset($_POST['description']) ? htmlspecialchars($_POST['description'], ENT_QUOTES, 'UTF-8') : ''; +$lat = isset($_POST['latitude']) ? floatval($_POST['latitude']) : 0.0; +$lng = isset($_POST['longitude']) ? floatval($_POST['longitude']) : 0.0; +$website = isset($_POST['website']) ? filter_var($_POST['website'], FILTER_VALIDATE_URL) : ''; +$telephone = validatePhoneNumber($_POST['telephone'] ?? '') ?: ''; + +if (empty($name)) { + http_response_code(400); + die('Campsite name is required.'); +} // Handle file upload $thumbnailPath = null; -if (isset($_FILES['thumbnail']) && $_FILES['thumbnail']['error'] == 0) { +if (isset($_FILES['thumbnail']) && $_FILES['thumbnail']['error'] !== UPLOAD_ERR_NO_FILE) { + // Validate file using hardened validation function + $validationResult = validateFileUpload($_FILES['thumbnail'], 'profile_picture'); + + if ($validationResult === false) { + http_response_code(400); + die('Invalid thumbnail image. Only JPG, JPEG, PNG, GIF, and WEBP images under 5MB are allowed.'); + } + $uploadDir = "assets/uploads/campsites/"; if (!is_dir($uploadDir)) { - mkdir($uploadDir, 0777, true); + mkdir($uploadDir, 0755, true); } - - $filename = time() . "_" . basename($_FILES["thumbnail"]["name"]); - $targetFile = $uploadDir . $filename; - + + if (!is_writable($uploadDir)) { + http_response_code(500); + die('Upload directory is not writable.'); + } + + $randomFilename = $validationResult['filename']; + $targetFile = $uploadDir . $randomFilename; + if (move_uploaded_file($_FILES["thumbnail"]["tmp_name"], $targetFile)) { + chmod($targetFile, 0644); $thumbnailPath = $targetFile; + } else { + http_response_code(500); + die('Failed to move uploaded file.'); } } @@ -48,14 +70,25 @@ if ($id > 0) { $stmt = $conn->prepare("UPDATE campsites SET name=?, description=?, latitude=?, longitude=?, website=?, telephone=? WHERE id=?"); $stmt->bind_param("ssddssi", $name, $desc, $lat, $lng, $website, $telephone, $id); } + + // Log the action + auditLog($user_id, 'CAMPSITE_UPDATE', 'campsites', $id, ['name' => $name]); } else { // INSERT $stmt = $conn->prepare("INSERT INTO campsites (name, description, latitude, longitude, website, telephone, thumbnail, user_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"); -$stmt->bind_param("ssddsssi", $name, $desc, $lat, $lng, $website, $telephone, $thumbnailPath, $user_id); - + $stmt->bind_param("ssddsssi", $name, $desc, $lat, $lng, $website, $telephone, $thumbnailPath, $user_id); + + // Log the action + auditLog($user_id, 'CAMPSITE_CREATE', 'campsites', 0, ['name' => $name]); } -$stmt->execute(); + +if (!$stmt->execute()) { + http_response_code(500); + die('Database error: ' . $stmt->error); +} + +$stmt->close(); header("Location: campsites.php"); ?> diff --git a/functions.php b/functions.php index 2f577635..cc79a920 100644 --- a/functions.php +++ b/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 ==================== diff --git a/submit_pop.php b/submit_pop.php index a587651f..892f5059 100644 --- a/submit_pop.php +++ b/submit_pop.php @@ -1,7 +1,6 @@ Invalid submission: missing eft_id or file."; - echo "
";
-        echo "POST data: " . print_r($_POST, true);
-        echo "FILES data: " . print_r($_FILES, true);
-        echo "
"; - } else { - $file = $_FILES['pop_file']; - $target_dir = "uploads/pop/"; - $target_file = $target_dir . $file_name . ".pdf"; - - // Check for upload errors first - if ($file['error'] !== UPLOAD_ERR_OK) { - echo "
Upload error code: " . $file['error'] . "
"; - // You can decode error code if needed: - // https://www.php.net/manual/en/features.file-upload.errors.php - exit; - } - - // Check for PDF extension - $file_type = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); - if ($file_type !== "pdf") { - echo "
Only PDF files allowed. You tried uploading: .$file_type
"; - exit; - } - - // Make sure target directory exists and writable - if (!is_dir($target_dir)) { - echo "
Upload directory does not exist: $target_dir
"; - exit; - } - if (!is_writable($target_dir)) { - echo "
Upload directory is not writable: $target_dir
"; - exit; - } - - if (move_uploaded_file($file['tmp_name'], $target_file)) { - chmod($target_file, 0664); - // Update EFT and booking status - $payment_type = $_POST['payment_type'] ?? 'booking'; - - if ($payment_type === 'membership') { - // Update EFT and booking status - $stmt1 = $conn->prepare("UPDATE efts SET status = 'PROCESSING' WHERE eft_id = ?"); - $stmt1->bind_param("s", $eft_id); - $stmt1->execute(); - // Update membership fee status - $stmt = $conn->prepare("UPDATE membership_fees SET payment_status = 'PROCESSING' WHERE payment_id = ?"); - $stmt->bind_param("s", $eft_id); - $stmt->execute(); - } else { - // Update EFT and booking status - $stmt1 = $conn->prepare("UPDATE efts SET status = 'PROCESSING' WHERE eft_id = ?"); - $stmt1->bind_param("s", $eft_id); - $stmt1->execute(); - - $stmt2 = $conn->prepare("UPDATE bookings SET status = 'PROCESSING' WHERE eft_id = ?"); - $stmt2->bind_param("s", $eft_id); - $stmt2->execute(); - } - - // Send notification email using sendPOP() - $fullname = getFullName($user_id); // Assuming this returns "First Last" + exit; + } + + // Validate file using hardened validation function + $validationResult = validateFileUpload($_FILES['pop_file'], 'proof_of_payment'); + + if ($validationResult === false) { + echo "
Invalid file. Only PDF files under 10MB are allowed.
"; + exit; + } + + $target_dir = "uploads/pop/"; + $randomFilename = $validationResult['filename']; + $target_file = $target_dir . $randomFilename; + + // Make sure target directory exists and writable + if (!is_dir($target_dir)) { + mkdir($target_dir, 0755, true); + } + + if (!is_writable($target_dir)) { + echo "
Upload directory is not writable: $target_dir
"; + exit; + } + + if (move_uploaded_file($_FILES['pop_file']['tmp_name'], $target_file)) { + chmod($target_file, 0644); + // Update EFT and booking status + $payment_type = $_POST['payment_type'] ?? 'booking'; + + if ($payment_type === 'membership') { + // Update EFT and booking status + $stmt1 = $conn->prepare("UPDATE efts SET status = 'PROCESSING' WHERE eft_id = ?"); + $stmt1->bind_param("s", $eft_id); + $stmt1->execute(); + $stmt1->close(); + + // Update membership fee status + $stmt = $conn->prepare("UPDATE membership_fees SET payment_status = 'PROCESSING' WHERE payment_id = ?"); + $stmt->bind_param("s", $eft_id); + $stmt->execute(); + $stmt->close(); + } else { + // Update EFT and booking status + $stmt1 = $conn->prepare("UPDATE efts SET status = 'PROCESSING' WHERE eft_id = ?"); + $stmt1->bind_param("s", $eft_id); + $stmt1->execute(); + $stmt1->close(); + + $stmt2 = $conn->prepare("UPDATE bookings SET status = 'PROCESSING' WHERE eft_id = ?"); + $stmt2->bind_param("s", $eft_id); + $stmt2->execute(); + $stmt2->close(); + } + + // Send notification email using sendPOP() + $fullname = getFullName($user_id); $eftDetails = getEFTDetails($eft_id); - $modified = str_replace(' ', '_', $eft_id); if ($eftDetails) { $amount = "R" . number_format($eftDetails['amount'], 2); $description = $eftDetails['description']; } else { $amount = "R0.00"; - $description = "Payment"; // fallback + $description = "Payment"; } - - if (sendPOP($fullname, $modified, $amount, $description)) { + if (sendPOP($fullname, $randomFilename, $amount, $description)) { $_SESSION['message'] = "Thank you! Your payment proof has been uploaded and notification sent."; } else { $_SESSION['message'] = "Payment uploaded, but notification email could not be sent."; } + + // Log the action + auditLog($user_id, 'POP_UPLOAD', 'efts', $eft_id, ['filename' => $randomFilename, 'payment_type' => $payment_type]); header("Location: bookings.php"); exit; @@ -158,7 +158,7 @@ if (!empty($bannerImages)) { 0) {?>
- +