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:
twotalesanimation
2025-12-03 11:28:53 +02:00
parent 062dc46ffd
commit 1ef4d06627
13 changed files with 1729 additions and 133 deletions

View File

@@ -13,8 +13,8 @@ if (!$conn) {
// Google Client Setup
$client = new Google_Client();
$client->setClientId('948441222188-8qhboq2urr8o9n35mc70s5h2nhd52v0m.apps.googleusercontent.com');
$client->setClientSecret('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');
$client->setClientId($_ENV['GOOGLE_CLIENT_ID']);
$client->setClientSecret($_ENV['GOOGLE_CLIENT_SECRET']);
$client->setRedirectUri($_ENV['HOST'] . '/validate_login.php');
$client->addScope("email");
$client->addScope("profile");
@@ -86,18 +86,57 @@ if (isset($_GET['code'])) {
// Check if email and password login is requested
if (isset($_POST['email']) && isset($_POST['password'])) {
// Retrieve and sanitize form data
$email = filter_var($_POST['email'], FILTER_SANITIZE_EMAIL);
$password = trim($_POST['password']); // Remove extra spaces
// Validate input
// CSRF Token Validation
if (!isset($_POST['csrf_token']) || !validateCSRFToken($_POST['csrf_token'])) {
auditLog(null, 'CSRF_VALIDATION_FAILED', 'users', null, ['endpoint' => 'validate_login.php']);
echo json_encode(['status' => 'error', 'message' => 'Security token validation failed. Please try again.']);
exit();
}
// Retrieve and validate email input
$email = validateEmail($_POST['email']);
if ($email === false) {
auditLog(null, 'INVALID_EMAIL_FORMAT', 'users', null, ['email' => $_POST['email']]);
echo json_encode(['status' => 'error', 'message' => 'Invalid email format.']);
exit();
}
// Retrieve and sanitize password
$password = isset($_POST['password']) ? trim($_POST['password']) : '';
// Basic validation
if (empty($email) || empty($password)) {
echo json_encode(['status' => 'error', 'message' => 'Please enter both email and password.']);
exit();
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
echo json_encode(['status' => 'error', 'message' => 'Invalid email format.']);
// Check for account lockout
$lockoutStatus = checkAccountLockout($email);
if ($lockoutStatus['is_locked']) {
auditLog(null, 'LOGIN_ATTEMPT_LOCKED_ACCOUNT', 'users', null, [
'email' => $email,
'locked_until' => $lockoutStatus['locked_until']
]);
echo json_encode([
'status' => 'error',
'message' => 'Account is temporarily locked due to multiple failed login attempts. Please try again in ' . $lockoutStatus['minutes_remaining'] . ' minutes.'
]);
exit();
}
// Check recent failed attempts
$recentFailedAttempts = countRecentFailedAttempts($email);
if ($recentFailedAttempts >= 5) {
// Lock account for 15 minutes
lockAccount($email, 15);
auditLog(null, 'ACCOUNT_LOCKED_THRESHOLD', 'users', null, [
'email' => $email,
'failed_attempts' => $recentFailedAttempts
]);
echo json_encode([
'status' => 'error',
'message' => 'Account locked due to multiple failed login attempts. Please try again in 15 minutes.'
]);
exit();
}
@@ -120,22 +159,55 @@ if (isset($_POST['email']) && isset($_POST['password'])) {
// Check if the user is verified
if ($row['is_verified'] == 0) {
recordLoginAttempt($email, false);
auditLog(null, 'LOGIN_ATTEMPT_UNVERIFIED_ACCOUNT', 'users', $row['user_id']);
echo json_encode(['status' => 'error', 'message' => 'Your account is not verified. Please check your email for the verification link.']);
exit();
}
if (password_verify($password, $row['password'])) {
// Record successful attempt
recordLoginAttempt($email, true);
// Regenerate session ID to prevent session fixation attacks
session_regenerate_id(true);
// Password is correct, set up session
$_SESSION['user_id'] = $row['user_id']; // Adjust as per your table structure
$_SESSION['first_name'] = $row['first_name']; // Adjust as per your table structure
$_SESSION['user_id'] = $row['user_id'];
$_SESSION['first_name'] = $row['first_name'];
$_SESSION['profile_pic'] = $row['profile_pic'];
// Set session timeout (30 minutes)
$_SESSION['login_time'] = time();
$_SESSION['session_timeout'] = 1800; // 30 minutes in seconds
auditLog($row['user_id'], 'LOGIN_SUCCESS', 'users', $row['user_id']);
echo json_encode(['status' => 'success', 'message' => 'Successful Login']);
} else {
// Password is incorrect
echo json_encode(['status' => 'error', 'message' => 'Invalid password.']);
// Password is incorrect - record failed attempt
recordLoginAttempt($email, false);
auditLog(null, 'LOGIN_FAILED_INVALID_PASSWORD', 'users', null, ['email' => $email]);
// Check if this was the threshold failure
$newFailureCount = countRecentFailedAttempts($email);
if ($newFailureCount >= 5) {
lockAccount($email, 15);
echo json_encode([
'status' => 'error',
'message' => 'Too many failed login attempts. Account locked for 15 minutes.'
]);
} else {
$attemptsRemaining = 5 - $newFailureCount;
echo json_encode([
'status' => 'error',
'message' => 'Invalid password. ' . $attemptsRemaining . ' attempts remaining before account lockout.'
]);
}
}
} else {
// User does not exist
// User does not exist - still record attempt
recordLoginAttempt($email, false);
auditLog(null, 'LOGIN_FAILED_USER_NOT_FOUND', 'users', null, ['email' => $email]);
echo json_encode(['status' => 'error', 'message' => 'User with that email does not exist.']);
}