From 94dbb795cc3fe9799d34beb5d6bfa052eba81b0c Mon Sep 17 00:00:00 2001 From: Thomas Vanbesien Date: Sun, 22 Mar 2026 13:57:45 +0100 Subject: 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. --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) (limited to 'README.md') diff --git a/README.md b/README.md index 57a2e2a..9056897 100644 --- a/README.md +++ b/README.md @@ -184,6 +184,20 @@ Nginx adds HTTP response headers that instruct the browser to enforce additional When a login fails, our error message says "Invalid username or password" rather than "Username not found" or "Wrong password". Similarly, the forgot-password form always says "If that email is registered, a reset link has been sent." This is deliberate: if the server told you *which* part was wrong, an attacker could probe the system to build a list of valid usernames or emails, then target those accounts specifically with brute-force or phishing attacks. +### Rate limiting (brute-force protection) + +Without rate limiting, an attacker can try thousands of passwords per second against a login form (a brute-force attack), or flood the password reset endpoint to spam a victim's inbox. Even strong passwords can be cracked given unlimited attempts — an 8-character password has a finite number of combinations, and automated tools can cycle through common passwords and variations very quickly. + +Camagru tracks attempts per IP address using a `rate_limits` database table. Each row records which IP performed which action and when. Before processing a login or password reset request, the `RateLimiter` class counts how many attempts that IP has made within a sliding time window. If the count exceeds the threshold, the request is rejected immediately — the password isn't even checked. + +**Limits enforced:** +- **Login:** 5 failed attempts per IP per 15 minutes. Only failed attempts are recorded — a successful login doesn't count against you. This means a legitimate user who knows their password is never blocked, but an attacker guessing passwords hits the wall after 5 tries. +- **Password reset:** 3 requests per IP per 15 minutes. Unlike login, *every* attempt is recorded (not just failures), because the reset endpoint doesn't reveal whether the email exists. If only "successful" resets counted, an attacker could probe for valid emails by watching which requests get rate-limited and which don't. + +**Why per-IP and not per-username?** If rate limiting were per-username, an attacker could lock out any user by deliberately failing logins against their account (a denial-of-service). Per-IP limiting means only the attacker's own IP gets blocked — the real user can still log in from their own network. The trade-off is that a distributed attack from many IPs can bypass this, but that level of attack is beyond the scope of a school project and would require infrastructure-level solutions (WAF, fail2ban, etc.). + +**Table cleanup:** to prevent the `rate_limits` table from growing indefinitely, old entries are purged on every check. Each call to `isLimited()` first deletes rows older than the time window, so only recent attempts are kept. This is a simple approach that works well for low-traffic applications — high-traffic production systems would typically use an in-memory store like Redis instead of a database table. + ### Upload security (preventing unwanted content) A classic web vulnerability is allowing users to upload arbitrary files — an attacker might upload a `.php` script disguised as an image and then request it directly to execute server-side code. Camagru prevents this with multiple layers: -- cgit v1.2.3