Compare commits

...

4 Commits

Author SHA1 Message Date
Yasen Pramatarov bc1089be21 Fixes whitelisting in db 2024-12-12 16:16:48 +02:00
Yasen Pramatarov a0747cfbc8 Adds whitelist table 2024-12-12 16:11:41 +02:00
Yasen Pramatarov 0f72f3bea4 Renames ratelimit table 2024-12-11 16:08:55 +02:00
Yasen Pramatarov 38e4b002c8 Adds comments 2024-12-11 16:00:13 +02:00
1 changed files with 61 additions and 38 deletions

View File

@ -4,30 +4,45 @@ class RateLimiter {
private $db; private $db;
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 $tableName = 'login_attempts'; private $ratelimitTable = 'login_attempts';
private $whitelistTable = 'ip_whitelist';
private $whitelistedIps = []; // Whitelisted IPs private $whitelistedIps = []; // Whitelisted IPs
private $whitelistedNetworks = []; // Whitelisted CIDR ranges private $whitelistedNetworks = []; // Whitelisted CIDR ranges
public function __construct($database) { public function __construct($database) {
$this->db = $database->getConnection(); $this->db = $database->getConnection();
$this->createTableIfNotExists(); $this->createTablesIfNotExists();
$this->loadWhitelist(); $this->loadWhitelist();
} }
private function createTableIfNotExists() { // Database preparation
$sql = "CREATE TABLE IF NOT EXISTS {$this->tableName} ( private function createTablesIfNotExists() {
// Login attempts table
$sql = "CREATE TABLE IF NOT EXISTS {$this->ratelimitTable} (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
ip_address VARCHAR(45) NOT NULL, ip_address VARCHAR(45) NOT NULL,
username VARCHAR(255) NOT NULL, username VARCHAR(255) NOT NULL,
attempted_at DATETIME DEFAULT CURRENT_TIMESTAMP, attempted_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_ip_username (ip_address, username) INDEX idx_ip_username (ip_address, username)
)"; )";
$this->db->exec($sql);
// IP whitelist table
$sql = "CREATE TABLE IF NOT EXISTS {$this->whitelistTable} (
id int(11) PRIMARY KEY AUTO_INCREMENT,
ip_address VARCHAR(45) NOT NULL,
is_network BOOLEAN DEFAULT FALSE,
description VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by VARCHAR(255),
UNIQUE KEY unique_ip (ip_address)
)";
$this->db->exec($sql); $this->db->exec($sql);
} }
// List of IPs to bypass rate limiting
private function loadWhitelist() { private function loadWhitelist() {
// Load from database or config // FIXME Load from database or config
$this->whitelistedIps = [ $this->whitelistedIps = [
'127.0.0.1', // localhost '127.0.0.1', // localhost
'::1' // localhost IPv6 '::1' // localhost IPv6
@ -40,20 +55,25 @@ class RateLimiter {
]; ];
} }
// Check if IP is whitelisted
private function isIpWhitelisted($ip) { private function isIpWhitelisted($ip) {
// Check exact IP match // Check exact IP match and CIDR ranges
if (in_array($ip, $this->whitelistedIps)) { $stmt = $this->db->prepare("SELECT ip_address, is_network FROM {$this->whitelistTable}");
return true; $stmt->execute();
}
// Check CIDR ranges while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
foreach ($this->whitelistedNetworks as $network) { if ($row['is_network']) {
if ($this->ipInRange($ip, $network)) { if ($this->ipInRange($ip, $row['ip_address'])) {
return true; return true;
} }
} } else {
if ($ip === $row['ip_address']) {
return true;
}
}
}
return false; return false;
} }
private function ipInRange($ip, $cidr) { private function ipInRange($ip, $cidr) {
@ -67,28 +87,31 @@ class RateLimiter {
return ($ip & $mask) == $subnet; return ($ip & $mask) == $subnet;
} }
public function addToWhitelist($ip, $isNetwork = false) { // Add to whitelist
if ($isNetwork) { public function addToWhitelist($ip, $isNetwork = false, $description = '', $createdBy = 'system') {
if (!in_array($ip, $this->whitelistedNetworks)) { $stmt = $this->db->prepare("INSERT INTO {$this->whitelistTable}
$this->whitelistedNetworks[] = $ip; (ip_address, is_network, description, created_by)
} VALUES (?, ?, ?, ?)
} else { ON DUPLICATE KEY UPDATE
if (!in_array($ip, $this->whitelistedIps)) { is_network = VALUES(is_network),
$this->whitelistedIps[] = $ip; description = VALUES(description),
} created_by = VALUES(created_by)");
}
return $stmt->execute([$ip, $isNetwork, $description, $createdBy]);
} }
// Remove from whitelist
public function removeFromWhitelist($ip) { public function removeFromWhitelist($ip) {
$indexIp = array_search($ip, $this->whitelistedIps); $stmt = $this->db->prepare("DELETE FROM {$this->whitelistTable} WHERE ip_address = ?");
if ($indexIp !== false) {
unset($this->whitelistedIps[$indexIp]);
}
$indexNetwork = array_search($ip, $this->whitelistedNetworks); return $stmt->execute([$ip]);
if ($indexNetwork !== false) { }
unset($this->whitelistedNetworks[$indexNetwork]);
} public function getWhitelistedIps() {
$stmt = $this->db->prepare("SELECT * FROM {$this->whitelistTable} ORDER BY created_at DESC");
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
} }
public function attempt($username, $ipAddress) { public function attempt($username, $ipAddress) {
@ -101,7 +124,7 @@ class RateLimiter {
$this->clearOldAttempts(); $this->clearOldAttempts();
// Record this attempt // Record this attempt
$sql = "INSERT INTO {$this->tableName} (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,
@ -114,7 +137,7 @@ class RateLimiter {
public function tooManyAttempts($username, $ipAddress) { public function tooManyAttempts($username, $ipAddress) {
$sql = "SELECT COUNT(*) as attempts $sql = "SELECT COUNT(*) as attempts
FROM {$this->tableName} 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 > datetime('now', '-' || :minutes || ' minutes')";
@ -131,7 +154,7 @@ class RateLimiter {
} }
public function clearOldAttempts() { public function clearOldAttempts() {
$sql = "DELETE FROM {$this->tableName} $sql = "DELETE FROM {$this->ratelimitTable}
WHERE attempted_at < datetime('now', '-' || :minutes || ' minutes')"; WHERE attempted_at < datetime('now', '-' || :minutes || ' minutes')";
$stmt = $this->db->prepare($sql); $stmt = $this->db->prepare($sql);
@ -142,7 +165,7 @@ class RateLimiter {
public function getRemainingAttempts($username, $ipAddress) { public function getRemainingAttempts($username, $ipAddress) {
$sql = "SELECT COUNT(*) as attempts $sql = "SELECT COUNT(*) as attempts
FROM {$this->tableName} 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 > datetime('now', '-' || :minutes || ' minutes')";