Fixes ratelimiting, adds auto blacklisting

main
Yasen Pramatarov 2025-01-04 11:32:19 +02:00
parent 8f32a79d0e
commit 50b74a15db
2 changed files with 61 additions and 21 deletions

View File

@ -5,6 +5,8 @@ class RateLimiter {
private $log; private $log;
private $maxAttempts = 5; // Maximum login attempts private $maxAttempts = 5; // Maximum login attempts
private $decayMinutes = 15; // Time window in minutes private $decayMinutes = 15; // Time window in minutes
private $autoBlacklistThreshold = 10; // Attempts before auto-blacklist
private $autoBlacklistDuration = 24; // Hours to blacklist for
private $ratelimitTable = 'login_attempts'; private $ratelimitTable = 'login_attempts';
private $whitelistTable = 'ip_whitelist'; private $whitelistTable = 'ip_whitelist';
private $blacklistTable = 'ip_blacklist'; private $blacklistTable = 'ip_blacklist';
@ -317,9 +319,9 @@ class RateLimiter {
$stmt->execute(); $stmt->execute();
// Clean old login attempts // Clean old login attempts
$stmt = $this->db->prepare("DELETE FROM {$this->tableName} $stmt = $this->db->prepare("DELETE FROM {$this->ratelimitTable}
WHERE attempted_at < DATE_SUB(NOW(), INTERVAL ? MINUTE)"); WHERE attempted_at < DATE_SUB(NOW(), INTERVAL :minutes MINUTE)");
$stmt->execute([$this->decayMinutes]); $stmt->execute([':minutes' => $this->decayMinutes]);
return true; return true;
} catch (Exception $e) { } catch (Exception $e) {
@ -328,8 +330,13 @@ class RateLimiter {
} }
} }
public function attempt($username, $ipAddress) { public function isAllowed($username, $ipAddress) {
// Skip rate limiting for whitelisted IPs // First check if IP is blacklisted
if ($this->isIpBlacklisted($ipAddress)) {
return false;
}
// Then check if IP is whitelisted
if ($this->isIpWhitelisted($ipAddress)) { if ($this->isIpWhitelisted($ipAddress)) {
return true; return true;
} }
@ -337,16 +344,50 @@ class RateLimiter {
// Clean old attempts // Clean old attempts
$this->clearOldAttempts(); $this->clearOldAttempts();
// Check if we've hit the rate limit
if ($this->tooManyAttempts($username, $ipAddress)) {
return false;
}
// Check total attempts across all usernames from this IP
$sql = "SELECT COUNT(*) as total_attempts
FROM {$this->ratelimitTable}
WHERE ip_address = :ip
AND attempted_at > DATE_SUB(NOW(), INTERVAL :minutes MINUTE)";
$stmt = $this->db->prepare($sql);
$stmt->execute([
':ip' => $ipAddress,
':minutes' => $this->decayMinutes
]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
// Check if we would hit auto-blacklist threshold
return $result['total_attempts'] < $this->autoBlacklistThreshold;
}
public function attempt($username, $ipAddress) {
// Record this attempt // Record this attempt
$sql = "INSERT INTO {$this->ratelimitTable} (ip_address, username) VALUES (:ip, :username)"; $sql = "INSERT INTO {$this->ratelimitTable} (ip_address, username) VALUES (:ip, :username)";
$stmt = $this->db->prepare($sql); $stmt = $this->db->prepare($sql);
$stmt->execute([ $stmt->execute([
':ip' => $ipAddress, ':ip' => $ipAddress,
':username' => $username ':username' => $username
]); ]);
// Check if too many attempts // Auto-blacklist if too many attempts
return !$this->tooManyAttempts($username, $ipAddress); if (!$this->isAllowed($username, $ipAddress)) {
$this->addToBlacklist(
$ipAddress,
false,
'Auto-blacklisted due to excessive login attempts',
'system',
null,
$this->autoBlacklistDuration
);
return false;
}
return true;
} }
public function tooManyAttempts($username, $ipAddress) { public function tooManyAttempts($username, $ipAddress) {
@ -354,7 +395,7 @@ class RateLimiter {
FROM {$this->ratelimitTable} FROM {$this->ratelimitTable}
WHERE ip_address = :ip WHERE ip_address = :ip
AND username = :username AND username = :username
AND attempted_at > datetime('now', '-' || :minutes || ' minutes')"; AND attempted_at > DATE_SUB(NOW(), INTERVAL :minutes MINUTE)";
$stmt = $this->db->prepare($sql); $stmt = $this->db->prepare($sql);
$stmt->execute([ $stmt->execute([
@ -369,7 +410,7 @@ class RateLimiter {
public function clearOldAttempts() { public function clearOldAttempts() {
$sql = "DELETE FROM {$this->ratelimitTable} $sql = "DELETE FROM {$this->ratelimitTable}
WHERE attempted_at < datetime('now', '-' || :minutes || ' minutes')"; WHERE attempted_at < DATE_SUB(NOW(), INTERVAL :minutes MINUTE)";
$stmt = $this->db->prepare($sql); $stmt = $this->db->prepare($sql);
$stmt->execute([ $stmt->execute([
@ -382,7 +423,7 @@ class RateLimiter {
FROM {$this->ratelimitTable} FROM {$this->ratelimitTable}
WHERE ip_address = :ip WHERE ip_address = :ip
AND username = :username AND username = :username
AND attempted_at > datetime('now', '-' || :minutes || ' minutes')"; AND attempted_at > DATE_SUB(NOW(), INTERVAL :minutes MINUTE)";
$stmt = $this->db->prepare($sql); $stmt = $this->db->prepare($sql);
$stmt->execute([ $stmt->execute([

View File

@ -93,12 +93,16 @@ class User {
// get client IP address // get client IP address
$ipAddress = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; $ipAddress = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
// check rate limiting // Record attempt
if (!$this->rateLimiter->attempt($username, $ipAddress)) { $this->rateLimiter->attempt($username, $ipAddress);
// Check rate limiting first
if (!$this->rateLimiter->isAllowed($username, $ipAddress)) {
$remainingTime = $this->rateLimiter->getDecayMinutes(); $remainingTime = $this->rateLimiter->getDecayMinutes();
throw new Exception("Too many login attempts. Please try again in {$remainingTime} minutes."); throw new Exception("Too many login attempts. Please try again in {$remainingTime} minutes.");
} }
// Then check credentials
$query = $this->db->prepare("SELECT * FROM users WHERE username = :username"); $query = $this->db->prepare("SELECT * FROM users WHERE username = :username");
$query->bindParam(':username', $username); $query->bindParam(':username', $username);
$query->execute(); $query->execute();
@ -110,14 +114,9 @@ class User {
return true; return true;
} }
// Login failed, return remaining attempts info // Get remaining attempts AFTER this failed attempt
$remainingAttempts = $this->rateLimiter->getRemainingAttempts($username, $ipAddress); $remainingAttempts = $this->rateLimiter->getRemainingAttempts($username, $ipAddress);
if ($remainingAttempts > 0) { throw new Exception("Invalid credentials. {$remainingAttempts} attempts remaining.");
throw new Exception("Invalid credentials. {$remainingAttempts} attempts remaining.");
} else {
$remainingTime = $this->rateLimiter->getDecayMinutes();
throw new Exception("Too many login attempts. Please try again in {$remainingTime} minutes.");
}
} }