diff --git a/app/classes/ratelimiter.php b/app/classes/ratelimiter.php index d808ae8..d912076 100644 --- a/app/classes/ratelimiter.php +++ b/app/classes/ratelimiter.php @@ -7,8 +7,8 @@ class RateLimiter { public $decayMinutes = 15; // Time window in minutes public $autoBlacklistThreshold = 10; // Attempts before auto-blacklist public $autoBlacklistDuration = 24; // Hours to blacklist for - public $authRatelimitTable = 'login_attempts'; - public $pagesRatelimitTable = 'pages_rate_limits'; + public $authRatelimitTable = 'login_attempts'; // For username/password attempts + public $pagesRatelimitTable = 'pages_rate_limits'; // For page requests public $whitelistTable = 'ip_whitelist'; public $blacklistTable = 'ip_blacklist'; private $pageLimits = [ @@ -33,7 +33,7 @@ class RateLimiter { // Authentication attempts table $sql = "CREATE TABLE IF NOT EXISTS {$this->authRatelimitTable} ( id INTEGER PRIMARY KEY AUTOINCREMENT, - ip_address TEXT NOT NULL UNIQUE, + ip_address TEXT NOT NULL, username TEXT NOT NULL, attempted_at TEXT DEFAULT (DATETIME('now')) )"; @@ -157,19 +157,24 @@ class RateLimiter { * Check if an IP is whitelisted */ public function isIpWhitelisted($ip) { - // Check exact IP match and CIDR ranges - $stmt = $this->db->prepare("SELECT ip_address, is_network FROM {$this->whitelistTable}"); - $stmt->execute(); + // Check exact IP match first + $stmt = $this->db->prepare("SELECT ip_address FROM {$this->whitelistTable} WHERE ip_address = ?"); + $stmt->execute([$ip]); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + if ($row) { + return true; + } - while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { - if ($row['is_network']) { + // Only check ranges for IPv4 addresses + if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { + // Check network ranges + $stmt = $this->db->prepare("SELECT ip_address FROM {$this->whitelistTable} WHERE is_network = 1"); + $stmt->execute(); + + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { if ($this->ipInRange($ip, $row['ip_address'])) { return true; } - } else { - if ($ip === $row['ip_address']) { - return true; - } } } @@ -177,8 +182,18 @@ class RateLimiter { } private function ipInRange($ip, $cidr) { + // Only work with IPv4 addresses + if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { + return false; + } + list($subnet, $bits) = explode('/', $cidr); + // Make sure subnet is IPv4 + if (!filter_var($subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { + return false; + } + $ip = ip2long($ip); $subnet = ip2long($subnet); $mask = -1 << (32 - $bits); @@ -412,21 +427,12 @@ class RateLimiter { // Record this attempt $sql = "INSERT INTO {$this->authRatelimitTable} (ip_address, username) VALUES (:ip, :username)"; $stmt = $this->db->prepare($sql); - $stmt->execute([ - ':ip' => $ipAddress, - ':username' => $username - ]); - - // Auto-blacklist if too many attempts - if (!$this->isAllowed($username, $ipAddress)) { - $this->addToBlacklist( - $ipAddress, - false, - 'Auto-blacklisted due to excessive login attempts', - 'system', - null, - $this->autoBlacklistDuration - ); + try { + $stmt->execute([ + ':ip' => $ipAddress, + ':username' => $username + ]); + } catch (PDOException $e) { return false; } @@ -448,6 +454,13 @@ class RateLimiter { ]); $result = $stmt->fetch(PDO::FETCH_ASSOC); + + // Also check what's in the table + $sql = "SELECT * FROM {$this->authRatelimitTable} WHERE ip_address = :ip"; + $stmt = $this->db->prepare($sql); + $stmt->execute([':ip' => $ipAddress]); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + return $result['attempts'] >= $this->maxAttempts; } diff --git a/app/includes/rate_limit_middleware.php b/app/includes/rate_limit_middleware.php index 48c48bc..32a09c3 100644 --- a/app/includes/rate_limit_middleware.php +++ b/app/includes/rate_limit_middleware.php @@ -9,12 +9,13 @@ require_once __DIR__ . '/../helpers/logs.php'; * @param Database $database Database connection * @param string $endpoint The endpoint being accessed * @param int|null $userId Current user ID if authenticated + * @param RateLimiter|null $existingRateLimiter Optional existing RateLimiter instance * @return bool True if request is allowed, false if rate limited */ -function checkRateLimit($database, $endpoint, $userId = null) { +function checkRateLimit($database, $endpoint, $userId = null, $existingRateLimiter = null) { global $app_root; $isTest = defined('PHPUNIT_RUNNING'); - $rateLimiter = new RateLimiter($database); + $rateLimiter = $existingRateLimiter ?? new RateLimiter($database); $ipAddress = getUserIP(); // Check if request is allowed diff --git a/app/pages/login.php b/app/pages/login.php index 7321387..3ff76bc 100644 --- a/app/pages/login.php +++ b/app/pages/login.php @@ -25,12 +25,11 @@ try { require_once '../app/classes/ratelimiter.php'; $rateLimiter = new RateLimiter($dbWeb); + // Get user IP + $user_IP = getUserIP(); + if ( $_SERVER['REQUEST_METHOD'] == 'POST' ) { try { - // apply page rate limiting - require_once '../app/includes/rate_limit_middleware.php'; - checkRateLimit($dbWeb, 'login', null); // null since user is not logged in yet - // Validate form data $security = SecurityHelper::getInstance(); $formData = $security->sanitizeArray($_POST, ['username', 'password', 'remember_me', 'csrf_token']); @@ -57,17 +56,20 @@ try { $username = $formData['username']; $password = $formData['password']; - // Check if IP is blacklisted - if ($rateLimiter->isIpBlacklisted($user_IP)) { - throw new Exception(Feedback::get('LOGIN', 'IP_BLACKLISTED')['message']); - } - - // Check rate limiting (but skip if IP is whitelisted) + // Skip all checks if IP is whitelisted if (!$rateLimiter->isIpWhitelisted($user_IP)) { - $attempts = $rateLimiter->getRecentAttempts($user_IP); - if ($attempts >= $rateLimiter->maxAttempts) { + // Check if IP is blacklisted + if ($rateLimiter->isIpBlacklisted($user_IP)) { + throw new Exception(Feedback::get('LOGIN', 'IP_BLACKLISTED')['message']); + } + + // Check rate limiting before recording attempt + if ($rateLimiter->tooManyAttempts($username, $user_IP)) { throw new Exception(Feedback::get('LOGIN', 'LOGIN_BLOCKED')['message']); } + + // Record this attempt + $rateLimiter->attempt($username, $user_IP); } // login successful @@ -77,22 +79,12 @@ try { // 30*24*60*60 = 30 days $cookie_lifetime = 30 * 24 * 60 * 60; $setcookie_lifetime = time() + 30 * 24 * 60 * 60; - $gc_maxlifetime = 30 * 24 * 60 * 60; } else { // 0 - session end on browser close - // 1440 - 24 minutes (default) $cookie_lifetime = 0; $setcookie_lifetime = 0; - $gc_maxlifetime = 1440; } - // Configure secure session settings - ini_set('session.cookie_httponly', 1); - ini_set('session.use_only_cookies', 1); - ini_set('session.cookie_secure', isset($_SERVER['HTTPS']) ? 1 : 0); - ini_set('session.cookie_samesite', 'Strict'); - ini_set('session.gc_maxlifetime', $gc_maxlifetime); - // Regenerate session ID to prevent session fixation session_regenerate_id(true);