Phase 1: Implement CSRF protection, input validation, and rate limiting
Major security improvements: - Added CSRF token generation, validation, and cleanup functions - Implemented comprehensive input validators (email, phone, name, date, amount, ID, file uploads) - Added rate limiting with login attempt tracking and account lockout (5 failures = 15 min lockout) - Implemented session fixation protection with session_regenerate_id() and 30-min timeout - Fixed SQL injection in getResultFromTable() with whitelisted columns/tables - Added audit logging for security events - Applied CSRF validation to all 7 process_*.php files - Applied input validation to critical endpoints (login, registration, bookings, application) - Created database migration for login_attempts, audit_log tables and locked_until column Modified files: - functions.php: +500 lines of security functions - validate_login.php: Added CSRF, rate limiting, session hardening - register_user.php: Added CSRF, input validation, registration rate limiting - process_*.php (7 files): Added CSRF token validation - Created migration: 001_phase1_security_schema.sql Next steps: Add CSRF tokens to form templates, harden file uploads, create testing checklist
This commit is contained in:
705
functions.php
705
functions.php
@@ -92,14 +92,12 @@ function calculateDaysAndNights($startDate, $endDate)
|
||||
|
||||
function sendVerificationEmail($email, $name, $token)
|
||||
{
|
||||
global $mailjet;
|
||||
|
||||
$message = [
|
||||
'Messages' => [
|
||||
[
|
||||
'From' => [
|
||||
'Email' => "info@4wdcsa.co.za",
|
||||
'Name' => "4WDCSA"
|
||||
'Email' => $_ENV['MAILJET_FROM_EMAIL'],
|
||||
'Name' => $_ENV['MAILJET_FROM_NAME']
|
||||
],
|
||||
'To' => [
|
||||
[
|
||||
@@ -119,36 +117,33 @@ function sendVerificationEmail($email, $name, $token)
|
||||
];
|
||||
|
||||
$client = new Client([
|
||||
// Base URI is used with relative requests
|
||||
'base_uri' => 'https://api.mailjet.com/v3.1/',
|
||||
]);
|
||||
|
||||
$response = $client->request('POST', 'send', [
|
||||
'json' => $message,
|
||||
'auth' => ['1a44f8d5e847537dbb8d3c76fe73a93c', 'ec98b45c53a7694c4f30d09eee9ad280']
|
||||
'auth' => [$_ENV['MAILJET_API_KEY'], $_ENV['MAILJET_API_SECRET']]
|
||||
]);
|
||||
|
||||
if ($response->getStatusCode() == 200) {
|
||||
$body = $response->getBody();
|
||||
$response = json_decode($body);
|
||||
if ($response->Messages[0]->Status == 'success') {
|
||||
return True;
|
||||
return true;
|
||||
} else {
|
||||
return False;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sendInvoice($email, $name, $eft_id, $amount, $description)
|
||||
{
|
||||
global $mailjet;
|
||||
|
||||
$message = [
|
||||
'Messages' => [
|
||||
[
|
||||
'From' => [
|
||||
'Email' => "info@4wdcsa.co.za",
|
||||
'Name' => "4WDCSA"
|
||||
'Email' => $_ENV['MAILJET_FROM_EMAIL'],
|
||||
'Name' => $_ENV['MAILJET_FROM_NAME']
|
||||
],
|
||||
'To' => [
|
||||
[
|
||||
@@ -169,22 +164,21 @@ function sendInvoice($email, $name, $eft_id, $amount, $description)
|
||||
];
|
||||
|
||||
$client = new Client([
|
||||
// Base URI is used with relative requests
|
||||
'base_uri' => 'https://api.mailjet.com/v3.1/',
|
||||
]);
|
||||
|
||||
$response = $client->request('POST', 'send', [
|
||||
'json' => $message,
|
||||
'auth' => ['1a44f8d5e847537dbb8d3c76fe73a93c', 'ec98b45c53a7694c4f30d09eee9ad280']
|
||||
'auth' => [$_ENV['MAILJET_API_KEY'], $_ENV['MAILJET_API_SECRET']]
|
||||
]);
|
||||
|
||||
if ($response->getStatusCode() == 200) {
|
||||
$body = $response->getBody();
|
||||
$response = json_decode($body);
|
||||
if ($response->Messages[0]->Status == 'success') {
|
||||
return True;
|
||||
return true;
|
||||
} else {
|
||||
return False;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -209,14 +203,12 @@ function getEFTDetails($eft_id) {
|
||||
|
||||
function sendPOP($fullname, $eft_id, $amount, $description)
|
||||
{
|
||||
global $mailjet;
|
||||
|
||||
$message = [
|
||||
'Messages' => [
|
||||
[
|
||||
'From' => [
|
||||
'Email' => "info@4wdcsa.co.za",
|
||||
'Name' => "4WDCSA Web Admin"
|
||||
'Email' => $_ENV['MAILJET_FROM_EMAIL'],
|
||||
'Name' => $_ENV['MAILJET_FROM_NAME'] . ' Web Admin'
|
||||
],
|
||||
'To' => [
|
||||
[
|
||||
@@ -224,7 +216,7 @@ function sendPOP($fullname, $eft_id, $amount, $description)
|
||||
'Name' => 'Chris Pinto'
|
||||
],
|
||||
[
|
||||
'Email' => 'info@4wdcsa.co.za',
|
||||
'Email' => $_ENV['MAILJET_FROM_EMAIL'],
|
||||
'Name' => 'Jacqui Boshoff'
|
||||
],
|
||||
[
|
||||
@@ -246,36 +238,33 @@ function sendPOP($fullname, $eft_id, $amount, $description)
|
||||
];
|
||||
|
||||
$client = new Client([
|
||||
// Base URI is used with relative requests
|
||||
'base_uri' => 'https://api.mailjet.com/v3.1/',
|
||||
]);
|
||||
|
||||
$response = $client->request('POST', 'send', [
|
||||
'json' => $message,
|
||||
'auth' => ['1a44f8d5e847537dbb8d3c76fe73a93c', 'ec98b45c53a7694c4f30d09eee9ad280']
|
||||
'auth' => [$_ENV['MAILJET_API_KEY'], $_ENV['MAILJET_API_SECRET']]
|
||||
]);
|
||||
|
||||
if ($response->getStatusCode() == 200) {
|
||||
$body = $response->getBody();
|
||||
$response = json_decode($body);
|
||||
if ($response->Messages[0]->Status == 'success') {
|
||||
return True;
|
||||
return true;
|
||||
} else {
|
||||
return False;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sendEmail($email, $subject, $message)
|
||||
{
|
||||
global $mailjet;
|
||||
|
||||
$message = [
|
||||
$messageData = [
|
||||
'Messages' => [
|
||||
[
|
||||
'From' => [
|
||||
'Email' => "info@4wdcsa.co.za",
|
||||
'Name' => "4WDCSA"
|
||||
'Email' => $_ENV['MAILJET_FROM_EMAIL'],
|
||||
'Name' => $_ENV['MAILJET_FROM_NAME']
|
||||
],
|
||||
'To' => [
|
||||
[
|
||||
@@ -289,36 +278,33 @@ function sendEmail($email, $subject, $message)
|
||||
];
|
||||
|
||||
$client = new Client([
|
||||
// Base URI is used with relative requests
|
||||
'base_uri' => 'https://api.mailjet.com/v3.1/',
|
||||
]);
|
||||
|
||||
$response = $client->request('POST', 'send', [
|
||||
'json' => $message,
|
||||
'auth' => ['1a44f8d5e847537dbb8d3c76fe73a93c', 'ec98b45c53a7694c4f30d09eee9ad280']
|
||||
'json' => $messageData,
|
||||
'auth' => [$_ENV['MAILJET_API_KEY'], $_ENV['MAILJET_API_SECRET']]
|
||||
]);
|
||||
|
||||
if ($response->getStatusCode() == 200) {
|
||||
$body = $response->getBody();
|
||||
$response = json_decode($body);
|
||||
if ($response->Messages[0]->Status == 'success') {
|
||||
return True;
|
||||
return true;
|
||||
} else {
|
||||
return False;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sendAdminNotification($subject, $message)
|
||||
{
|
||||
global $mailjet;
|
||||
|
||||
$mail = [
|
||||
'Messages' => [
|
||||
[
|
||||
'From' => [
|
||||
'Email' => "info@4wdcsa.co.za",
|
||||
'Name' => "4WDCSA"
|
||||
'Email' => $_ENV['MAILJET_FROM_EMAIL'],
|
||||
'Name' => $_ENV['MAILJET_FROM_NAME']
|
||||
],
|
||||
'To' => [
|
||||
[
|
||||
@@ -337,36 +323,33 @@ function sendAdminNotification($subject, $message)
|
||||
];
|
||||
|
||||
$client = new Client([
|
||||
// Base URI is used with relative requests
|
||||
'base_uri' => 'https://api.mailjet.com/v3.1/',
|
||||
]);
|
||||
|
||||
$response = $client->request('POST', 'send', [
|
||||
'json' => $mail,
|
||||
'auth' => ['1a44f8d5e847537dbb8d3c76fe73a93c', 'ec98b45c53a7694c4f30d09eee9ad280']
|
||||
'auth' => [$_ENV['MAILJET_API_KEY'], $_ENV['MAILJET_API_SECRET']]
|
||||
]);
|
||||
|
||||
if ($response->getStatusCode() == 200) {
|
||||
$body = $response->getBody();
|
||||
$response = json_decode($body);
|
||||
if ($response->Messages[0]->Status == 'success') {
|
||||
return True;
|
||||
return true;
|
||||
} else {
|
||||
return False;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sendPaymentConfirmation($email, $name, $description)
|
||||
{
|
||||
global $mailjet;
|
||||
|
||||
$message = [
|
||||
'Messages' => [
|
||||
[
|
||||
'From' => [
|
||||
'Email' => "info@4wdcsa.co.za",
|
||||
'Name' => "4WDCSA"
|
||||
'Email' => $_ENV['MAILJET_FROM_EMAIL'],
|
||||
'Name' => $_ENV['MAILJET_FROM_NAME']
|
||||
],
|
||||
'To' => [
|
||||
[
|
||||
@@ -386,22 +369,21 @@ function sendPaymentConfirmation($email, $name, $description)
|
||||
];
|
||||
|
||||
$client = new Client([
|
||||
// Base URI is used with relative requests
|
||||
'base_uri' => 'https://api.mailjet.com/v3.1/',
|
||||
]);
|
||||
|
||||
$response = $client->request('POST', 'send', [
|
||||
'json' => $message,
|
||||
'auth' => ['1a44f8d5e847537dbb8d3c76fe73a93c', 'ec98b45c53a7694c4f30d09eee9ad280']
|
||||
'auth' => [$_ENV['MAILJET_API_KEY'], $_ENV['MAILJET_API_SECRET']]
|
||||
]);
|
||||
|
||||
if ($response->getStatusCode() == 200) {
|
||||
$body = $response->getBody();
|
||||
$response = json_decode($body);
|
||||
if ($response->Messages[0]->Status == 'success') {
|
||||
return True;
|
||||
return true;
|
||||
} else {
|
||||
return False;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -591,7 +573,7 @@ function processPayment($payment_id, $amount, $description)
|
||||
{
|
||||
$conn = openDatabaseConnection();
|
||||
$status = "AWAITING PAYMENT";
|
||||
$domain = 'www.thepinto.co.za/4wdcsa';
|
||||
$domain = $_ENV['PAYFAST_DOMAIN'];
|
||||
$user_id = $_SESSION['user_id'];
|
||||
// Insert the order into the orders table
|
||||
$stmt = $conn->prepare("
|
||||
@@ -625,7 +607,7 @@ function processPayment($payment_id, $amount, $description)
|
||||
* @param null $passPhrase
|
||||
* @return string
|
||||
*/
|
||||
function generateSignature($data, $passPhrase = 'SheSells7Shells')
|
||||
function generateSignature($data, $passPhrase = null)
|
||||
{
|
||||
// Create parameter string
|
||||
$pfOutput = '';
|
||||
@@ -644,8 +626,8 @@ function processPayment($payment_id, $amount, $description)
|
||||
// Construct variables
|
||||
$data = array(
|
||||
// Merchant details
|
||||
'merchant_id' => '10021495',
|
||||
'merchant_key' => 'yzpdydo934j92',
|
||||
'merchant_id' => $_ENV['PAYFAST_MERCHANT_ID'],
|
||||
'merchant_key' => $_ENV['PAYFAST_MERCHANT_KEY'],
|
||||
'return_url' => 'https://' . $domain . '/bookings.php',
|
||||
'cancel_url' => 'https://' . $domain . '/cancel_booking.php?booking_id=' . $encryptedId,
|
||||
'notify_url' => 'https://' . $domain . '/confirm.php',
|
||||
@@ -659,11 +641,11 @@ function processPayment($payment_id, $amount, $description)
|
||||
'item_name' => '4WDCSA: ' . $description // Describe the item(s) or use a generic description
|
||||
);
|
||||
|
||||
$signature = generateSignature($data); // Assuming you have this function defined
|
||||
$signature = generateSignature($data, $_ENV['PAYFAST_PASSPHRASE']);
|
||||
$data['signature'] = $signature;
|
||||
|
||||
// Determine the PayFast URL based on the mode
|
||||
$testingMode = true;
|
||||
$testingMode = $_ENV['PAYFAST_TESTING_MODE'] === 'true';
|
||||
$pfHost = $testingMode ? 'sandbox.payfast.co.za' : 'www.payfast.co.za';
|
||||
|
||||
// Generate the HTML form with hidden inputs and an auto-submit script
|
||||
@@ -690,7 +672,7 @@ function processMembershipPayment($payment_id, $amount, $description)
|
||||
{
|
||||
$conn = openDatabaseConnection();
|
||||
$status = "AWAITING PAYMENT";
|
||||
$domain = 'www.thepinto.co.za/4wdcsa';
|
||||
$domain = $_ENV['PAYFAST_DOMAIN'];
|
||||
$user_id = $_SESSION['user_id'];
|
||||
// Insert the order into the orders table
|
||||
$stmt = $conn->prepare("
|
||||
@@ -724,7 +706,7 @@ function processMembershipPayment($payment_id, $amount, $description)
|
||||
* @param null $passPhrase
|
||||
* @return string
|
||||
*/
|
||||
function generateSignature($data, $passPhrase = 'SheSells7Shells')
|
||||
function generateSignature($data, $passPhrase = null)
|
||||
{
|
||||
// Create parameter string
|
||||
$pfOutput = '';
|
||||
@@ -743,8 +725,8 @@ function processMembershipPayment($payment_id, $amount, $description)
|
||||
// Construct variables
|
||||
$data = array(
|
||||
// Merchant details
|
||||
'merchant_id' => '10021495',
|
||||
'merchant_key' => 'yzpdydo934j92',
|
||||
'merchant_id' => $_ENV['PAYFAST_MERCHANT_ID'],
|
||||
'merchant_key' => $_ENV['PAYFAST_MERCHANT_KEY'],
|
||||
'return_url' => 'https://' . $domain . '/account_settings.php',
|
||||
'cancel_url' => 'https://' . $domain . '/cancel_application.php?id=' . $encryptedId,
|
||||
'notify_url' => 'https://' . $domain . '/confirm2.php',
|
||||
@@ -758,11 +740,11 @@ function processMembershipPayment($payment_id, $amount, $description)
|
||||
'item_name' => $description // Describe the item(s) or use a generic description
|
||||
);
|
||||
|
||||
$signature = generateSignature($data); // Assuming you have this function defined
|
||||
$signature = generateSignature($data, $_ENV['PAYFAST_PASSPHRASE']);
|
||||
$data['signature'] = $signature;
|
||||
|
||||
// Determine the PayFast URL based on the mode
|
||||
$testingMode = true;
|
||||
$testingMode = $_ENV['PAYFAST_TESTING_MODE'] === 'true';
|
||||
$pfHost = $testingMode ? 'sandbox.payfast.co.za' : 'www.payfast.co.za';
|
||||
|
||||
// Generate the HTML form with hidden inputs and an auto-submit script
|
||||
@@ -1904,16 +1886,84 @@ function processLegacyMembership($user_id) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SECURITY WARNING: This function uses dynamic table/column names which makes it vulnerable to SQL injection.
|
||||
* ONLY call this function with whitelisted table and column names.
|
||||
* NEVER accept table/column names directly from user input.
|
||||
*
|
||||
* Retrieves a single value from a database table.
|
||||
* @param string $table Table name (MUST be whitelisted - see allowed_tables array)
|
||||
* @param string $column Column name to retrieve (MUST be whitelisted - see allowed_columns array)
|
||||
* @param string $match Column name for WHERE clause (MUST be whitelisted)
|
||||
* @param mixed $identifier Value to match in WHERE clause (parameterized - safe)
|
||||
* @return mixed The result value or null if not found
|
||||
*/
|
||||
function getResultFromTable($table, $column, $match, $identifier) {
|
||||
// WHITELIST: Define allowed tables to prevent table name injection
|
||||
$allowed_tables = [
|
||||
'users',
|
||||
'membership_application',
|
||||
'membership_fees',
|
||||
'bookings',
|
||||
'payments',
|
||||
'efts',
|
||||
'trips',
|
||||
'courses',
|
||||
'blogs',
|
||||
'events',
|
||||
'campsites',
|
||||
'bar_transactions',
|
||||
'login_attempts',
|
||||
'legacy_members'
|
||||
];
|
||||
|
||||
// WHITELIST: Define allowed columns per table (simplified - add more as needed)
|
||||
$allowed_columns = [
|
||||
'legacy_members' => ['amount', 'legacy_id', 'email', 'name'],
|
||||
'users' => ['user_id', 'email', 'first_name', 'last_name', 'phone_number', 'password', 'profile_pic', 'is_verified', 'type', 'locked_until'],
|
||||
'membership_fees' => ['payment_id', 'user_id', 'amount', 'payment_status', 'payment_date'],
|
||||
'bookings' => ['booking_id', 'user_id', 'total_amount', 'status', 'booking_type'],
|
||||
'payments' => ['payment_id', 'user_id', 'amount', 'status'],
|
||||
'trips' => ['trip_id', 'trip_name', 'description'],
|
||||
'courses' => ['course_id', 'course_name', 'description'],
|
||||
'blogs' => ['blog_id', 'title', 'content'],
|
||||
'events' => ['event_id', 'event_name', 'description'],
|
||||
'campsites' => ['campsite_id', 'name', 'location'],
|
||||
'efts' => ['eft_id', 'amount', 'status', 'booking_id'],
|
||||
'bar_transactions' => ['transaction_id', 'amount', 'date'],
|
||||
'login_attempts' => ['attempt_id', 'email', 'ip_address', 'success']
|
||||
];
|
||||
|
||||
$conn = openDatabaseConnection();
|
||||
$sql = "SELECT `$column` FROM `$table` WHERE `$match` = ?";
|
||||
$stmt = $conn->prepare($sql);
|
||||
if (!$stmt) {
|
||||
// Validate table name is in whitelist
|
||||
if (!in_array($table, $allowed_tables, true)) {
|
||||
error_log("Security Warning: getResultFromTable() called with non-whitelisted table: $table");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Validate column name is in whitelist for this table
|
||||
if (!isset($allowed_columns[$table]) || !in_array($column, $allowed_columns[$table], true)) {
|
||||
error_log("Security Warning: getResultFromTable() called with non-whitelisted column: $column for table: $table");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Validate match column is in whitelist for this table
|
||||
if (!isset($allowed_columns[$table]) || !in_array($match, $allowed_columns[$table], true)) {
|
||||
error_log("Security Warning: getResultFromTable() called with non-whitelisted match column: $match for table: $table");
|
||||
return null;
|
||||
}
|
||||
|
||||
$stmt->bind_param('i', $identifier);
|
||||
$conn = openDatabaseConnection();
|
||||
// Use backticks for table and column identifiers (safe after whitelist validation)
|
||||
$sql = "SELECT `" . $column . "` FROM `" . $table . "` WHERE `" . $match . "` = ?";
|
||||
$stmt = $conn->prepare($sql);
|
||||
if (!$stmt) {
|
||||
error_log("Database prepare error: " . $conn->error);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Determine parameter type based on identifier
|
||||
$paramType = is_int($identifier) ? 'i' : 's';
|
||||
$stmt->bind_param($paramType, $identifier);
|
||||
$stmt->execute();
|
||||
$stmt->bind_result($result);
|
||||
$stmt->fetch();
|
||||
@@ -1977,3 +2027,522 @@ function hasPhoneNumber($user_id) {
|
||||
// Return true only if a phone number exists and is not empty
|
||||
return !empty($phone_number);
|
||||
}
|
||||
|
||||
// ==================== CSRF PROTECTION FUNCTIONS ====================
|
||||
|
||||
/**
|
||||
* Generates a CSRF token and stores it in the session with expiration
|
||||
* @param int $duration Token expiration time in seconds (default 3600 = 1 hour)
|
||||
* @return string The generated CSRF token
|
||||
*/
|
||||
function generateCSRFToken($duration = 3600) {
|
||||
// Initialize CSRF token storage in session if needed
|
||||
if (!isset($_SESSION['csrf_tokens'])) {
|
||||
$_SESSION['csrf_tokens'] = [];
|
||||
}
|
||||
|
||||
// Clean up expired tokens
|
||||
cleanupExpiredTokens();
|
||||
|
||||
// Generate a random token
|
||||
$token = bin2hex(random_bytes(32));
|
||||
|
||||
// Store token with expiration timestamp
|
||||
$_SESSION['csrf_tokens'][$token] = time() + $duration;
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a CSRF token from user input
|
||||
* @param string $token The token to validate (typically from $_POST['csrf_token'])
|
||||
* @return bool True if token is valid, false otherwise
|
||||
*/
|
||||
function validateCSRFToken($token) {
|
||||
// Check if token exists in session
|
||||
if (!isset($_SESSION['csrf_tokens']) || !isset($_SESSION['csrf_tokens'][$token])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if token has expired
|
||||
if ($_SESSION['csrf_tokens'][$token] < time()) {
|
||||
unset($_SESSION['csrf_tokens'][$token]);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Token is valid - remove it from session (single-use)
|
||||
unset($_SESSION['csrf_tokens'][$token]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes expired tokens from the session
|
||||
*/
|
||||
function cleanupExpiredTokens() {
|
||||
if (!isset($_SESSION['csrf_tokens'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$currentTime = time();
|
||||
foreach ($_SESSION['csrf_tokens'] as $token => $expiration) {
|
||||
if ($expiration < $currentTime) {
|
||||
unset($_SESSION['csrf_tokens'][$token]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== INPUT VALIDATION FUNCTIONS ====================
|
||||
|
||||
/**
|
||||
* Validates and sanitizes email input
|
||||
* @param string $email The email to validate
|
||||
* @param int $maxLength Maximum allowed length (default 254 per RFC 5321)
|
||||
* @return string|false Sanitized email or false if invalid
|
||||
*/
|
||||
function validateEmail($email, $maxLength = 254) {
|
||||
// Check length
|
||||
if (strlen($email) > $maxLength) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Filter and validate
|
||||
$filtered = filter_var($email, FILTER_VALIDATE_EMAIL);
|
||||
|
||||
// Sanitize if valid
|
||||
return $filtered ? filter_var($filtered, FILTER_SANITIZE_EMAIL) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates phone number format
|
||||
* @param string $phone The phone number to validate
|
||||
* @return string|false Sanitized phone number or false if invalid
|
||||
*/
|
||||
function validatePhoneNumber($phone) {
|
||||
// Remove common formatting characters
|
||||
$cleaned = preg_replace('/[^\d+\-\s().]/', '', $phone);
|
||||
|
||||
// Check length (between 7 and 20 digits)
|
||||
$digitCount = strlen(preg_replace('/[^\d]/', '', $cleaned));
|
||||
if ($digitCount < 7 || $digitCount > 20) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $cleaned;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates and sanitizes a name (first name, last name)
|
||||
* @param string $name The name to validate
|
||||
* @param int $minLength Minimum allowed length (default 2)
|
||||
* @param int $maxLength Maximum allowed length (default 100)
|
||||
* @return string|false Sanitized name or false if invalid
|
||||
*/
|
||||
function validateName($name, $minLength = 2, $maxLength = 100) {
|
||||
// Trim whitespace
|
||||
$name = trim($name);
|
||||
|
||||
// Check length
|
||||
if (strlen($name) < $minLength || strlen($name) > $maxLength) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only allow letters, spaces, hyphens, and apostrophes
|
||||
if (!preg_match('/^[a-zA-Z\s\'-]+$/', $name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return htmlspecialchars($name, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a date string in YYYY-MM-DD format
|
||||
* @param string $date The date string to validate
|
||||
* @param string $format Expected date format (default 'Y-m-d')
|
||||
* @return string|false Valid date string or false if invalid
|
||||
*/
|
||||
function validateDate($date, $format = 'Y-m-d') {
|
||||
$d = DateTime::createFromFormat($format, $date);
|
||||
|
||||
// Check if date is valid and in correct format
|
||||
if (!$d || $d->format($format) !== $date) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a numeric amount (for currency)
|
||||
* @param mixed $amount The amount to validate
|
||||
* @param float $min Minimum allowed amount (default 0)
|
||||
* @param float $max Maximum allowed amount (default 999999.99)
|
||||
* @return float|false Valid amount or false if invalid
|
||||
*/
|
||||
function validateAmount($amount, $min = 0, $max = 999999.99) {
|
||||
// Try to convert to float
|
||||
$value = filter_var($amount, FILTER_VALIDATE_FLOAT, [
|
||||
'options' => [
|
||||
'min_range' => $min,
|
||||
'max_range' => $max,
|
||||
'decimal' => '.'
|
||||
]
|
||||
]);
|
||||
|
||||
// Must have at most 2 decimal places
|
||||
if ($value !== false) {
|
||||
$parts = explode('.', (string)$amount);
|
||||
if (isset($parts[1]) && strlen($parts[1]) > 2) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates an integer within a range
|
||||
* @param mixed $int The integer to validate
|
||||
* @param int $min Minimum allowed value (default 0)
|
||||
* @param int $max Maximum allowed value (default 2147483647)
|
||||
* @return int|false Valid integer or false if invalid
|
||||
*/
|
||||
function validateInteger($int, $min = 0, $max = 2147483647) {
|
||||
$value = filter_var($int, FILTER_VALIDATE_INT, [
|
||||
'options' => [
|
||||
'min_range' => $min,
|
||||
'max_range' => $max
|
||||
]
|
||||
]);
|
||||
|
||||
return $value !== false ? $value : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates South African ID number (13 digits)
|
||||
* @param string $idNumber The ID number to validate
|
||||
* @return string|false Valid ID number or false if invalid
|
||||
*/
|
||||
function validateSAIDNumber($idNumber) {
|
||||
// Remove any whitespace
|
||||
$idNumber = preg_replace('/\s/', '', $idNumber);
|
||||
|
||||
// Must be exactly 13 digits
|
||||
if (!preg_match('/^\d{13}$/', $idNumber)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Optional: Validate checksum (Luhn algorithm)
|
||||
$sum = 0;
|
||||
for ($i = 0; $i < 13; $i++) {
|
||||
$digit = (int)$idNumber[$i];
|
||||
|
||||
// Double every even-positioned digit (0-indexed)
|
||||
if ($i % 2 == 0) {
|
||||
$digit *= 2;
|
||||
if ($digit > 9) {
|
||||
$digit -= 9;
|
||||
}
|
||||
}
|
||||
|
||||
$sum += $digit;
|
||||
}
|
||||
|
||||
// Last digit should make sum divisible by 10
|
||||
if ($sum % 10 != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $idNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes text input, removing potentially dangerous characters
|
||||
* @param string $text The text to sanitize
|
||||
* @param int $maxLength Maximum allowed length
|
||||
* @return string Sanitized text
|
||||
*/
|
||||
function sanitizeTextInput($text, $maxLength = 1000) {
|
||||
// Trim whitespace
|
||||
$text = trim($text);
|
||||
|
||||
// Limit length
|
||||
$text = substr($text, 0, $maxLength);
|
||||
|
||||
// Encode HTML special characters
|
||||
return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates file uploads for security
|
||||
* @param array $file The $_FILES element to validate
|
||||
* @param array $allowedTypes Array of allowed MIME types (e.g., ['image/jpeg', 'image/png', 'application/pdf'])
|
||||
* @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) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check file size
|
||||
if (!isset($file['size']) || $file['size'] > $maxSize) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check MIME type
|
||||
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||
$mimeType = finfo_file($finfo, $file['tmp_name']);
|
||||
finfo_close($finfo);
|
||||
|
||||
if (!in_array($mimeType, $allowedTypes, true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Generate random filename with original extension
|
||||
$pathinfo = pathinfo($file['name']);
|
||||
$extension = strtolower($pathinfo['extension']);
|
||||
|
||||
// Whitelist allowed extensions
|
||||
$allowedExtensions = ['jpg', 'jpeg', 'png', 'pdf', 'gif', 'webp'];
|
||||
if (!in_array($extension, $allowedExtensions)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Generate random filename
|
||||
$randomName = bin2hex(random_bytes(16)) . '.' . $extension;
|
||||
|
||||
return $randomName;
|
||||
}
|
||||
|
||||
// ==================== RATE LIMITING & ACCOUNT LOCKOUT FUNCTIONS ====================
|
||||
|
||||
/**
|
||||
* Records a login attempt in the login_attempts table
|
||||
* @param string $email The email address attempting to login
|
||||
* @param bool $success Whether the login was successful
|
||||
* @return void
|
||||
*/
|
||||
function recordLoginAttempt($email, $success = false) {
|
||||
// Get client IP address
|
||||
$ip = getClientIPAddress();
|
||||
|
||||
$conn = openDatabaseConnection();
|
||||
if (!$conn) {
|
||||
return;
|
||||
}
|
||||
|
||||
$email = strtolower(trim($email));
|
||||
|
||||
$sql = "INSERT INTO login_attempts (email, ip_address, success) VALUES (?, ?, ?)";
|
||||
$stmt = $conn->prepare($sql);
|
||||
if ($stmt) {
|
||||
$success_int = $success ? 1 : 0;
|
||||
$stmt->bind_param('ssi', $email, $ip, $success_int);
|
||||
$stmt->execute();
|
||||
$stmt->close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the client's IP address safely
|
||||
* @return string The client's IP address
|
||||
*/
|
||||
function getClientIPAddress() {
|
||||
// Check for IP from share internet
|
||||
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
|
||||
$ip = $_SERVER['HTTP_CLIENT_IP'];
|
||||
}
|
||||
// Check for IP passed from proxy
|
||||
elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||||
$ip = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0];
|
||||
}
|
||||
// Check for remote IP
|
||||
else {
|
||||
$ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
|
||||
}
|
||||
|
||||
// Validate IP format
|
||||
if (!filter_var($ip, FILTER_VALIDATE_IP)) {
|
||||
$ip = '0.0.0.0';
|
||||
}
|
||||
|
||||
return $ip;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an account is locked and returns lockout status
|
||||
* @param string $email The email address to check
|
||||
* @return array ['is_locked' => bool, 'locked_until' => datetime string or null, 'minutes_remaining' => int]
|
||||
*/
|
||||
function checkAccountLockout($email) {
|
||||
$conn = openDatabaseConnection();
|
||||
if (!$conn) {
|
||||
return ['is_locked' => false, 'locked_until' => null, 'minutes_remaining' => 0];
|
||||
}
|
||||
|
||||
$email = strtolower(trim($email));
|
||||
$now = date('Y-m-d H:i:s');
|
||||
|
||||
$sql = "SELECT locked_until FROM users WHERE email = ? LIMIT 1";
|
||||
$stmt = $conn->prepare($sql);
|
||||
if (!$stmt) {
|
||||
return ['is_locked' => false, 'locked_until' => null, 'minutes_remaining' => 0];
|
||||
}
|
||||
|
||||
$stmt->bind_param('s', $email);
|
||||
$stmt->execute();
|
||||
$stmt->bind_result($locked_until);
|
||||
$stmt->fetch();
|
||||
$stmt->close();
|
||||
|
||||
if ($locked_until === null) {
|
||||
return ['is_locked' => false, 'locked_until' => null, 'minutes_remaining' => 0];
|
||||
}
|
||||
|
||||
if ($locked_until > $now) {
|
||||
// Account is still locked
|
||||
$lockTime = strtotime($locked_until);
|
||||
$nowTime = strtotime($now);
|
||||
$secondsRemaining = max(0, $lockTime - $nowTime);
|
||||
$minutesRemaining = ceil($secondsRemaining / 60);
|
||||
|
||||
return [
|
||||
'is_locked' => true,
|
||||
'locked_until' => $locked_until,
|
||||
'minutes_remaining' => $minutesRemaining
|
||||
];
|
||||
}
|
||||
|
||||
// Lockout has expired, clear it
|
||||
$sql = "UPDATE users SET locked_until = NULL WHERE email = ?";
|
||||
$stmt = $conn->prepare($sql);
|
||||
if ($stmt) {
|
||||
$stmt->bind_param('s', $email);
|
||||
$stmt->execute();
|
||||
$stmt->close();
|
||||
}
|
||||
|
||||
return ['is_locked' => false, 'locked_until' => null, 'minutes_remaining' => 0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts recent failed login attempts for an email + IP combination
|
||||
* @param string $email The email address
|
||||
* @param int $minutesBack How many minutes back to check (default 15)
|
||||
* @return int Number of failed attempts
|
||||
*/
|
||||
function countRecentFailedAttempts($email, $minutesBack = 15) {
|
||||
$conn = openDatabaseConnection();
|
||||
if (!$conn) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$email = strtolower(trim($email));
|
||||
$ip = getClientIPAddress();
|
||||
$cutoffTime = date('Y-m-d H:i:s', time() - ($minutesBack * 60));
|
||||
|
||||
$sql = "SELECT COUNT(*) as count FROM login_attempts
|
||||
WHERE email = ? AND ip_address = ? AND success = 0
|
||||
AND attempted_at > ?";
|
||||
$stmt = $conn->prepare($sql);
|
||||
if (!$stmt) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$stmt->bind_param('sss', $email, $ip, $cutoffTime);
|
||||
$stmt->execute();
|
||||
$stmt->bind_result($count);
|
||||
$stmt->fetch();
|
||||
$stmt->close();
|
||||
|
||||
return (int)$count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Locks an account for a specified duration
|
||||
* @param string $email The email address to lock
|
||||
* @param int $minutes Duration of lockout in minutes (default 15)
|
||||
* @return bool True if successful, false otherwise
|
||||
*/
|
||||
function lockAccount($email, $minutes = 15) {
|
||||
$conn = openDatabaseConnection();
|
||||
if (!$conn) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$email = strtolower(trim($email));
|
||||
$lockUntil = date('Y-m-d H:i:s', time() + ($minutes * 60));
|
||||
|
||||
$sql = "UPDATE users SET locked_until = ? WHERE email = ?";
|
||||
$stmt = $conn->prepare($sql);
|
||||
if (!$stmt) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$stmt->bind_param('ss', $lockUntil, $email);
|
||||
$result = $stmt->execute();
|
||||
$stmt->close();
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlocks an account (admin function)
|
||||
* @param string $email The email address to unlock
|
||||
* @return bool True if successful, false otherwise
|
||||
*/
|
||||
function unlockAccount($email) {
|
||||
$conn = openDatabaseConnection();
|
||||
if (!$conn) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$email = strtolower(trim($email));
|
||||
|
||||
$sql = "UPDATE users SET locked_until = NULL WHERE email = ?";
|
||||
$stmt = $conn->prepare($sql);
|
||||
if (!$stmt) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$stmt->bind_param('s', $email);
|
||||
$result = $stmt->execute();
|
||||
$stmt->close();
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs an action to the audit log table
|
||||
* @param int $user_id User ID (null if not authenticated)
|
||||
* @param string $action Action name (e.g., 'LOGIN', 'FAILED_LOGIN', 'ACCOUNT_LOCKED')
|
||||
* @param string $resource_type Resource type being affected
|
||||
* @param int $resource_id Resource ID being affected
|
||||
* @param array $details Additional details about the action
|
||||
* @return bool True if successful
|
||||
*/
|
||||
function auditLog($user_id, $action, $resource_type = null, $resource_id = null, $details = null) {
|
||||
$conn = openDatabaseConnection();
|
||||
if (!$conn) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$ip = getClientIPAddress();
|
||||
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? '';
|
||||
$detailsJson = $details ? json_encode($details) : null;
|
||||
|
||||
$sql = "INSERT INTO audit_log (user_id, action, resource_type, resource_id, ip_address, user_agent, details)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)";
|
||||
$stmt = $conn->prepare($sql);
|
||||
if (!$stmt) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$stmt->bind_param('issdsss', $user_id, $action, $resource_type, $resource_id, $ip, $userAgent, $detailsJson);
|
||||
$result = $stmt->execute();
|
||||
$stmt->close();
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user