Files
4WDCSA.co.za/submit_pop.php
twotalesanimation b120415d53 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
2025-12-03 13:30:45 +02:00

212 lines
8.6 KiB
PHP

<?php include_once('header02.php');
require_once("functions.php");
checkUserSession();
$user_id = $_SESSION['user_id'] ?? null;
if (!$user_id) {
die("Not logged in.");
}
// Handle POST submission
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// CSRF Token Validation
if (!isset($_POST['csrf_token']) || !validateCSRFToken($_POST['csrf_token'])) {
http_response_code(403);
die('Security token validation failed. Please try again.');
}
$eft_id = $_POST['eft_id'] ?? null;
if (!$eft_id || !isset($_FILES['pop_file'])) {
echo "<div class='alert alert-danger'>Invalid submission: missing eft_id or file.</div>";
exit;
}
// Validate file using hardened validation function
$validationResult = validateFileUpload($_FILES['pop_file'], 'proof_of_payment');
if ($validationResult === false) {
echo "<div class='alert alert-danger'>Invalid file. Only PDF files under 10MB are allowed.</div>";
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 "<div class='alert alert-danger'>Upload directory is not writable: $target_dir</div>";
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);
if ($eftDetails) {
$amount = "R" . number_format($eftDetails['amount'], 2);
$description = $eftDetails['description'];
} else {
$amount = "R0.00";
$description = "Payment";
}
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;
} else {
echo "<div class='alert alert-danger'>Unable to move uploaded file.</div>";
exit;
}
}
// Fetch bookings for dropdown
$stmt = $conn->prepare("
SELECT eft_id AS id, 'booking' AS type FROM bookings WHERE user_id = ? AND status = 'AWAITING PAYMENT'
UNION
SELECT payment_id AS id, 'membership' AS type FROM membership_fees WHERE user_id = ? AND payment_status = 'PENDING'
");
$stmt->bind_param("ii", $user_id, $user_id);
$stmt->execute();
$result = $stmt->get_result();
$items = $result->fetch_all(MYSQLI_ASSOC);
$bannerFolder = 'assets/images/banners/';
$bannerImages = glob($bannerFolder . '*.{jpg,jpeg,png,webp}', GLOB_BRACE);
$randomBanner = 'assets/images/base4/camping.jpg'; // default fallback
if (!empty($bannerImages)) {
$randomBanner = $bannerImages[array_rand($bannerImages)];
}
?>
<section class="page-banner-area pt-50 pb-35 rel z-1 bgs-cover" style="background-image: url('<?php echo $randomBanner; ?>');">
<div class="banner-overlay"></div>
<div class="container">
<div class="banner-inner text-white mb-50">
<h2 class="page-title mb-10" data-aos="fade-left" data-aos-duration="1500" data-aos-offset="50">Submit Proof of Payment</h2>
<nav aria-label="breadcrumb">
<ol class="breadcrumb justify-content-center mb-20" data-aos="fade-right" data-aos-delay="200" data-aos-duration="1500" data-aos-offset="50">
<li class="breadcrumb-item"><a href="index.php">Home</a></li>
<li class="breadcrumb-item active">Submit Proof of Payment</li>
</ol>
</nav>
</div>
</div>
</section>
<!-- Tour List Area start -->
<section class="tour-list-page py-100 rel z-1">
<div class="container" style="max-width:600px;">
<div class="row">
<div class="comment-form bgc-lighter z-1 rel mb-30 rmb-55" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
<div class="widget widget-booking" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
<div class="section-title">
<h3>Submit Proof of Payment</h3>
<div style="text-align: center;" id="responseMessage"></div>
<p>To finalise your booking/membership, select the payment reference below, and then upload your PDF proof of payment.</p> <!-- Message display area -->
</div>
<?php if (count($items) > 0) {?>
<form enctype="multipart/form-data" method="POST">
<input type="hidden" name="csrf_token" value="<?php echo generateCSRFToken(); ?>">
<div class="row mt-35">
<ul class="tickets clearfix">
<li>
Select Payment Reference:
<select name="eft_id" id="eft_id" required onchange="updatePaymentType(this)">
<?php
if (count($items) > 0) {
foreach ($items as $item) {
$label = strtoupper($item['type']) . ' - ' . htmlspecialchars($item['id']);
echo '<option value="' . htmlspecialchars($item['id']) . '" data-type="' . $item['type'] . '">' . $label . '</option>';
}
} else {
echo '<option value="" disabled selected>No payments available</option>';
}
?>
</select>
<input type="hidden" name="payment_type" id="payment_type">
</li>
</ul>
<li>
<input style="border-radius:30px;" type="file" name="pop_file" id="pop_file" accept="application/pdf" class="form-control" required>
</li>
</div>
<div class="mt-10 mb-0">
<button type="submit" class="theme-btn style-two" style="width:100%;">Submit POP</button>
</div>
</form>
<?php
}else{
echo 'No unpaid bookings';
}?>
</div>
</div>
</div>
</div>
</section>
<script>
function updatePaymentType(selectEl) {
const selectedOption = selectEl.options[selectEl.selectedIndex];
const type = selectedOption.getAttribute('data-type');
document.getElementById('payment_type').value = type;
}
window.onload = function() {
const dropdown = document.getElementById('eft_id');
updatePaymentType(dropdown); // set default value on page load
};
</script>
<?php include_once("insta_footer.php"); ?>