diff options
| author | Thomas Vanbesien <tvanbesi@proton.me> | 2026-03-22 13:57:45 +0100 |
|---|---|---|
| committer | Thomas Vanbesien <tvanbesi@proton.me> | 2026-03-22 13:57:45 +0100 |
| commit | 94dbb795cc3fe9799d34beb5d6bfa052eba81b0c (patch) | |
| tree | 7b7d60a977dba7339431b2b1ff5d10121a016d08 /src/app/RateLimiter.php | |
| parent | 78e891f06ab94ef478de1c431157f7d634fe4ac8 (diff) | |
| download | camagru-94dbb795cc3fe9799d34beb5d6bfa052eba81b0c.tar.gz camagru-94dbb795cc3fe9799d34beb5d6bfa052eba81b0c.zip | |
Add rate limiting on login and password reset endpoints
Track attempts per IP in a rate_limits table with a sliding time
window. Login allows 5 failed attempts per 15 min, password reset
allows 3 requests per 15 min. Old entries are purged automatically.
Diffstat (limited to 'src/app/RateLimiter.php')
| -rw-r--r-- | src/app/RateLimiter.php | 60 |
1 files changed, 60 insertions, 0 deletions
diff --git a/src/app/RateLimiter.php b/src/app/RateLimiter.php new file mode 100644 index 0000000..07b46ba --- /dev/null +++ b/src/app/RateLimiter.php @@ -0,0 +1,60 @@ +<?php + +declare(strict_types=1); +// Rate limiter: prevents brute-force attacks by tracking how many times an IP +// performs a given action (e.g. login, password reset) within a sliding time window. + +namespace App; + +class RateLimiter +{ + private \PDO $pdo; + + public function __construct() + { + $this->pdo = Database::getInstance()->getPdo(); + } + + /** + * Check whether the given IP has exceeded the allowed number of attempts + * for an action within the time window. + */ + public function isLimited(string $ip, string $action, int $maxAttempts, int $windowSeconds): bool + { + // Clean up old entries first so the table doesn't grow forever + $this->purge($action, $windowSeconds); + + $stmt = $this->pdo->prepare( + 'SELECT COUNT(*) FROM rate_limits + WHERE ip = :ip AND action = :action + AND attempted_at > DATE_SUB(NOW(), INTERVAL :window SECOND)' + ); + $stmt->execute(['ip' => $ip, 'action' => $action, 'window' => $windowSeconds]); + + return (int) $stmt->fetchColumn() >= $maxAttempts; + } + + /** + * Record an attempt for the given IP and action. + */ + public function record(string $ip, string $action): void + { + $stmt = $this->pdo->prepare( + 'INSERT INTO rate_limits (ip, action) VALUES (:ip, :action)' + ); + $stmt->execute(['ip' => $ip, 'action' => $action]); + } + + /** + * Remove entries older than the window so the table stays small. + */ + private function purge(string $action, int $windowSeconds): void + { + $stmt = $this->pdo->prepare( + 'DELETE FROM rate_limits + WHERE action = :action + AND attempted_at <= DATE_SUB(NOW(), INTERVAL :window SECOND)' + ); + $stmt->execute(['action' => $action, 'window' => $windowSeconds]); + } +} |
