aboutsummaryrefslogtreecommitdiffstats
path: root/src/app/Controllers/AuthController.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/Controllers/AuthController.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/Controllers/AuthController.php')
-rw-r--r--src/app/Controllers/AuthController.php32
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