Fixes ratelimiting, adds auto blacklisting
parent
8f32a79d0e
commit
50b74a15db
|
@ -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([
|
||||||
|
|
|
@ -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.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue