Brute-Force Protection
Also Known As
TL;DR
Explanation
Brute-force attacks submit large volumes of credential combinations to authentication endpoints until one succeeds. Defence layers: (1) Rate limiting — restrict attempts per IP and per account (e.g. 5 failures per 10 minutes) using a token-bucket or sliding-window counter stored in Redis or APCu. (2) Progressive delays — exponential back-off after each failure slows automated tools without locking legitimate users out permanently. (3) Account lockout — temporarily disable an account after N failures; prefer time-based lockout over permanent to avoid denial-of-service. (4) CAPTCHA — challenges that are easy for humans and hard for bots, triggered after a threshold of failures. (5) MFA — a second factor renders the guessed password alone useless. In PHP, rate limiting is typically implemented in middleware or a firewall layer before the authentication logic. Credential stuffing (using breached credential databases) is a variant — defeated by checking submitted passwords against known-breached lists (HaveIBeenPwned API). Timing-safe comparison (hash_equals) prevents timing-based oracle attacks that can leak valid usernames.
Watch Out
Common Misconception
Why It Matters
Common Mistakes
- Rate limiting only by IP — attackers distribute requests across thousands of IPs; always also count per-username.
- Permanent account lockout — creates a denial-of-service vector where an attacker locks every account by deliberate failures.
- Not applying rate limits to password-reset endpoints — reset flows are equally brute-forceable and often less protected.
- Leaking whether a username exists via different error messages — use a generic 'invalid credentials' message regardless of which field is wrong.
Code Examples
// No rate limiting — unlimited attempts:
public function login(string $email, string $password): bool {
$user = User::findByEmail($email);
return $user && password_verify($password, $user->password_hash);
}
// Redis-backed per-account rate limit:
public function login(string $email, string $password): bool {
$key = 'login_attempts:' . hash('sha256', $email);
$attempts = (int) $this->redis->get($key);
if ($attempts >= 5) {
throw new TooManyAttemptsException('Account temporarily locked');
}
$user = User::findByEmail($email);
if (!$user || !password_verify($password, $user->password_hash)) {
$this->redis->incr($key);
$this->redis->expire($key, 900); // 15 min window
throw new InvalidCredentialsException('Invalid credentials');
}
$this->redis->del($key); // reset on success
return true;
}