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/Controllers | |
| 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/Controllers')
| -rw-r--r-- | src/app/Controllers/AuthController.php | 32 |
1 files changed, 32 insertions, 0 deletions
diff --git a/src/app/Controllers/AuthController.php b/src/app/Controllers/AuthController.php index aad40be..d1ac746 100644 --- a/src/app/Controllers/AuthController.php +++ b/src/app/Controllers/AuthController.php @@ -9,14 +9,24 @@ use App\Csrf; use App\Flash; use App\Mail; use App\Models\User; +use App\RateLimiter; class AuthController { + // 5 failed logins per 15 minutes per IP + private const LOGIN_MAX_ATTEMPTS = 5; + private const LOGIN_WINDOW = 900; + // 3 password reset requests per 15 minutes per IP + private const RESET_MAX_ATTEMPTS = 3; + private const RESET_WINDOW = 900; + private User $user; + private RateLimiter $limiter; public function __construct() { $this->user = new User(); + $this->limiter = new RateLimiter(); } public function registerForm(): void @@ -107,6 +117,14 @@ class AuthController return; } + $ip = $_SERVER['REMOTE_ADDR'] ?? ''; + + if ($this->limiter->isLimited($ip, 'login', self::LOGIN_MAX_ATTEMPTS, self::LOGIN_WINDOW)) { + Flash::set('error', 'Too many login attempts. Please try again later.'); + header('Location: /login'); + return; + } + $username = trim($_POST['username'] ?? ''); $password = $_POST['password'] ?? ''; @@ -115,6 +133,8 @@ class AuthController // Use the same error message for wrong username or wrong password // to avoid revealing which usernames exist (user enumeration) if (!$user || !password_verify($password, $user['password_hash'])) { + // Only record failed attempts — successful logins don't count + $this->limiter->record($ip, 'login'); Flash::set('error', 'Invalid username or password.'); header('Location: /login'); return; @@ -154,6 +174,18 @@ class AuthController return; } + $ip = $_SERVER['REMOTE_ADDR'] ?? ''; + + if ($this->limiter->isLimited($ip, 'reset', self::RESET_MAX_ATTEMPTS, self::RESET_WINDOW)) { + Flash::set('error', 'Too many reset requests. Please try again later.'); + header('Location: /forgot-password'); + return; + } + + // Record every attempt — even for non-existent emails, to prevent + // an attacker from probing email addresses at high speed + $this->limiter->record($ip, 'reset'); + $email = trim($_POST['email'] ?? ''); // Always show the same message whether the email exists or not |
