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

@@ -1,7 +1,9 @@
<?php
session_start();
include_once('connection.php'); // DB connection file
include_once('connection.php');
require_once("functions.php");
require_once("env.php");
$response = array('status' => 'error', 'message' => 'Something went wrong');
// Check if the user is logged in
@@ -14,50 +16,60 @@ if (!isset($_SESSION['user_id'])) {
$user_id = $_SESSION['user_id'];
// Handle profile picture upload
if (isset($_FILES['profile_picture']['name']) && $_FILES['profile_picture']['error'] == 0) {
if (isset($_FILES['profile_picture']) && $_FILES['profile_picture']['error'] != UPLOAD_ERR_NO_FILE) {
// Validate file using hardened validation function
$validationResult = validateFileUpload($_FILES['profile_picture'], 'profile_picture');
if ($validationResult === false) {
$response['message'] = 'Invalid file. Only JPG, JPEG, PNG, GIF, and WEBP images under 5MB are allowed.';
echo json_encode($response);
exit();
}
// Extract validated filename
$randomFilename = $validationResult['filename'];
$target_dir = "assets/images/pp/";
$imageFileType = strtolower(pathinfo($_FILES["profile_picture"]["name"], PATHINFO_EXTENSION));
// Set the target file as $user_id.EXT (where EXT is the image's extension)
$target_file = $target_dir . $user_id . '.' . $imageFileType;
$filename = $user_id . '.' . $imageFileType;
// Check if the uploaded file is an image
$check = getimagesize($_FILES["profile_picture"]["tmp_name"]);
if ($check !== false) {
// Limit the file size to 5MB
if ($_FILES["profile_picture"]["size"] > 5000000) {
$response['message'] = 'Sorry, your file is too large.';
$target_file = $target_dir . $randomFilename;
// Ensure upload directory exists and is writable
if (!is_dir($target_dir)) {
mkdir($target_dir, 0755, true);
}
if (!is_writable($target_dir)) {
$response['message'] = 'Upload directory is not writable.';
echo json_encode($response);
exit();
}
// Move the uploaded file
if (move_uploaded_file($_FILES['profile_picture']['tmp_name'], $target_file)) {
// Set secure file permissions (readable but not executable)
chmod($target_file, 0644);
// Update the profile picture path in the database
$sql = "UPDATE users SET profile_pic = ? WHERE user_id = ?";
$stmt = $conn->prepare($sql);
if (!$stmt) {
$response['message'] = 'Database error.';
echo json_encode($response);
exit();
}
// Allow certain file formats
$allowed_types = array("jpg", "jpeg", "png", "gif");
if (!in_array($imageFileType, $allowed_types)) {
$response['message'] = 'Sorry, only JPG, JPEG, PNG & GIF files are allowed.';
echo json_encode($response);
exit();
}
// Move the uploaded file to the server and name it as $user_id.EXT
if (move_uploaded_file($_FILES["profile_picture"]["tmp_name"], $target_file)) {
// Update the profile picture path in the database
$sql = "UPDATE users SET profile_pic = ? WHERE user_id = ?";
$stmt = $conn->prepare($sql);
$stmt->bind_param("si", $target_file, $user_id);
if ($stmt->execute()) {
$_SESSION['profile_pic'] = $target_file;
$response['status'] = 'success';
$response['message'] = 'Profile picture updated successfully';
} else {
$response['message'] = 'Failed to update profile picture in the database';
}
$stmt->bind_param("si", $target_file, $user_id);
if ($stmt->execute()) {
$_SESSION['profile_pic'] = $target_file;
$response['status'] = 'success';
$response['message'] = 'Profile picture updated successfully';
// Log the action
auditLog($user_id, 'PROFILE_PIC_UPLOAD', 'users', $user_id, ['filename' => $randomFilename]);
} else {
$response['message'] = 'Sorry, there was an error uploading your file.';
$response['message'] = 'Failed to update profile picture in the database';
}
$stmt->close();
} else {
$response['message'] = 'File is not an image.';
$response['message'] = 'Failed to move uploaded file.';
}
} else {
$response['message'] = 'No file uploaded or file error.';