aboutsummaryrefslogtreecommitdiffstats
path: root/src/app/RateLimiter.php
diff options
context:
space:
mode:
authorThomas Vanbesien <tvanbesi@proton.me>2026-03-22 13:57:45 +0100
committerThomas Vanbesien <tvanbesi@proton.me>2026-03-22 13:57:45 +0100
commit94dbb795cc3fe9799d34beb5d6bfa052eba81b0c (patch)
tree7b7d60a977dba7339431b2b1ff5d10121a016d08 /src/app/RateLimiter.php
parent78e891f06ab94ef478de1c431157f7d634fe4ac8 (diff)
downloadcamagru-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.php60
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]);
+ }
+}