Skip to content
intermediate Phase 9 · Password Storage and Session Security

Brute-Force Defenses and Rate Limiting

Slow credential guessing with throttling, lockouts, and backoff.

45m
0 problems
Topic Progress 0%

Understanding Brute-Force and Credential Stuffing Attacks

Brute-Force and Credential Stuffing: Two Different Attacks Requiring Different Defenses

Brute-force and credential stuffing are both password-guessing attacks, but they differ fundamentally in their approach and the defenses that work against them.

Brute-Force Attacks

A brute-force attack systematically tries many passwords against a single account:

  • Online brute-force: The attacker sends login requests directly to the application, trying passwords from a dictionary or generating them systematically.
  • Offline brute-force: The attacker obtains a password hash database and cracks it locally using GPU clusters. No interaction with the application occurs.

Online brute-force characteristics:

  • Targets a single account with many password attempts
  • Slow — limited by network latency and server response time
  • Detectable — generates many failed login attempts from a single source
  • Defended by: rate limiting, account lockouts, CAPTCHA

Offline brute-force characteristics:

  • Not detectable by the application — no login attempts are made
  • Extremely fast — billions of guesses per second on GPU clusters
  • Defended by: strong password hashing (Argon2id, bcrypt), password complexity requirements

Credential Stuffing

Credential stuffing is an attack where an attacker replays username and password pairs leaked from other services:

  • The attacker obtains a list of credentials from a data breach (e.g., from Have I Been Pwned or dark web marketplaces)
  • They use automated tools to try these credential pairs on many different websites
  • The attack exploits password reuse — users who use the same password across multiple services

Credential stuffing characteristics:

  • Targets many accounts with a single password attempt each (or a few attempts per account)
  • Fast — distributed across thousands of IP addresses to avoid rate limits
  • Hard to detect — each IP makes only a few requests, appearing as normal login attempts
  • Defended by: MFA, breach password detection, device fingerprinting, bot detection

Why Rate Limiting Alone Is Not Enough

Rate limiting is effective against online brute-force but insufficient against credential stuffing:

  • Brute-force: One IP, many attempts → rate limiting catches it
  • Credential stuffing: Many IPs, few attempts each → rate limiting misses it

A comprehensive defense requires multiple layers:

Layer Brute-Force Credential Stuffing
Per-account rate limit Effective Partially effective
Per-IP rate limit Effective Ineffective (distributed)
CAPTCHA Effective Partially effective
MFA Effective Effective
Breach password check Ineffective Effective
Bot detection Effective Effective
Device fingerprinting Effective Effective

Implementing Rate Limiting and Progressive Lockouts

Practical Rate Limiting and Lockout Strategies

Per-Account Rate Limiting

Limit the number of failed login attempts per account within a time window:

import time
from collections import defaultdict

# Simple in-memory rate limiter (use Redis in production)
failed_attempts = defaultdict(list)

MAX_ATTEMPTS = 5
WINDOW_SECONDS = 300  # 5 minutes

def is_account_locked(username):
    now = time.time()
    attempts = failed_attempts[username]
    
    # Remove attempts outside the window
    attempts[:] = [t for t in attempts if now - t < WINDOW_SECONDS]
    
    return len(attempts) >= MAX_ATTEMPTS

def record_failed_attempt(username):
    failed_attempts[username].append(time.time())

def login(username, password):
    if is_account_locked(username):
        return {'error': 'Account temporarily locked. Try again later.'}, 429
    
    if verify_password(username, password):
        failed_attempts[username].clear()  # Reset on success
        return {'status': 'success'}
    else:
        record_failed_attempt(username)
        return {'error': 'Invalid credentials'}, 401

Per-IP Rate Limiting

Limit the number of login requests per IP address:

from flask_limiter import Limiter
from flask_limiter.util import get_remote_address

limiter = Limiter(
    key_func=get_remote_address,
    default_limits=["200 per day", "50 per hour"]
)

@app.route('/login', methods=['POST'])
@limiter.limit("5 per minute")  # Strict limit for login endpoint
def login():
    # ...

Progressive Lockouts

Instead of a hard lockout (which creates a denial-of-service vector), use progressive delays:

def get_delay(failed_count):
    """Exponential backoff based on failed attempt count."""
    if failed_count <= 3:
        return 0  # No delay for first 3 attempts
    return min(2 ** (failed_count - 3), 30)  # Max 30 seconds

def login(username, password):
    user = get_user(username)
    
    if user:
        delay = get_delay(user.failed_login_count)
        if delay > 0:
            time.sleep(delay)  # Slow down the response
        
        if verify_password(password, user.password_hash):
            user.failed_login_count = 0
            return success()
        else:
            user.failed_login_count += 1
            return failure()

Why progressive lockouts are better than hard lockouts:

  • Hard lockout: After 5 failed attempts, the account is locked for 30 minutes. An attacker can deliberately lock out any user (denial of service).
  • Progressive delay: Each failed attempt increases the response time. The attacker is slowed down but legitimate users are not locked out.

CAPTCHA Integration

After a threshold of failed attempts, require a CAPTCHA:

def login(username, password):
    user = get_user(username)
    
    if user and user.failed_login_count >= 3:
        if not verify_captcha(request.form.get('captcha_token')):
            return {'error': 'CAPTCHA required'}, 403
    
    # ... proceed with login

Breach Password Detection

Check new passwords against known breach corpora (e.g., Have I Been Pwned's Pwned Passwords API):

import hashlib
import requests

def is_pwned_password(password):
    """Check if password appears in known data breaches using k-anonymity."""
    sha1 = hashlib.sha1(password.encode()).hexdigest().upper()
    prefix, suffix = sha1[:5], sha1[5:]
    
    response = requests.get(f'https://api.pwnedpasswords.com/range/{prefix}')
    hashes = response.text.splitlines()
    
    for h in hashes:
        if h.split(':')[0] == suffix:
            return True  # Password found in breach
    return False

This uses k-anonymity — only the first 5 characters of the hash are sent to the API, so the full password is never transmitted.

Practice: Design a Rate Limiting Strategy

Design a rate limiting strategy for a login endpoint that:

  1. Limits each account to 5 failed attempts per 15 minutes
  2. Limits each IP to 20 login attempts per minute
  3. Requires CAPTCHA after 3 failed attempts
  4. Does not permanently lock any account

Practice Problems

0 / 2 solved
Brute-Force vs. Credential Stuffing

An attacker obtains a list of 10 million username/password pairs from a data breach. They use a botnet of 10,000 IP addresses to try these credentials on your login endpoint, making 100 requests per minute from each IP. Explain why per-IP rate limiting alone is insufficient and what additional defenses you would implement.

Solution
Per-IP rate limiting is insufficient because the attack is distributed across 10,000 IPs, each making only 100 requests per minute — well below typical per-IP rate limits. Each IP tries 100 different username/password pairs, so the total attempt volume is 1 million requests per minute, but no single IP triggers a rate limit.

Additional defenses:
1. **MFA**: Even if credentials are correct, the attacker cannot complete login without the second factor
2. **Breach password detection**: Check passwords against known breach corpora and reject or flag them
3. **Bot detection**: Analyze request patterns (timing, headers, JavaScript execution) to distinguish bots from humans
4. **Device fingerprinting**: Track devices and flag suspicious patterns (many accounts from one device)
5. **Per-account rate limiting**: Limit failed attempts per account across all IPs (not per-IP)
6. **CAPTCHA**: Require CAPTCHA for login attempts from suspicious sources
Implement Progressive Delays

Implement a progressive delay system for failed login attempts. The delay should increase exponentially: 0s for the first 3 attempts, then 2s, 4s, 8s, 16s, with a maximum of 30s.

Solution
```python
import time

def get_delay(failed_count):
    if failed_count <= 3:
        return 0
    return min(2 ** (failed_count - 3), 30)

def login(username, password):
    user = get_user(username)
    
    if not user:
        # Use constant-time comparison to prevent username enumeration
        dummy_hash = '$argon2id$v=19$m=65536,t=3,p=4$...'
        verify_password(password, dummy_hash)
        return {'error': 'Invalid credentials'}, 401
    
    delay = get_delay(user.failed_login_count)
    if delay > 0:
        time.sleep(delay)
    
    if verify_password(password, user.password_hash):
        user.failed_login_count = 0
        user.save()
        return {'status': 'success'}
    else:
        user.failed_login_count += 1
        user.save()
        return {'error': 'Invalid credentials'}, 401

# Delay progression:
# Attempt 1-3: 0s delay
# Attempt 4: 2s delay
# Attempt 5: 4s delay
# Attempt 6: 8s delay
# Attempt 7: 16s delay
# Attempt 8+: 30s delay (capped)
```

This slows brute-force attacks exponentially while not locking out legitimate users who occasionally mistype their password.

Quiz

1. What is the key difference between brute-force and credential stuffing attacks?

Question 1 options

2. Why are progressive delays preferred over hard account lockouts?

Question 2 options

3. Which defense is most effective against credential stuffing attacks?

Question 3 options

4. What is the purpose of checking passwords against breach corpora (e.g., Have I Been Pwned)?

Question 4 options

5. Why is a dummy hash comparison used when a username does not exist?

Question 5 options

Flashcards

Question

What is the difference between brute-force and credential stuffing?

Answer

Brute-force targets one account with many password attempts. Credential stuffing replays leaked username/password pairs across many accounts. Rate limiting defends against brute-force; MFA and breach password detection defend against credential stuffing.

Question

Why are progressive delays preferred over hard lockouts?

Answer

Hard lockouts create a denial-of-service vector — an attacker can deliberately lock out any user. Progressive delays slow attackers exponentially without locking out legitimate users.

Question

What is the most effective defense against credential stuffing?

Answer

Multi-factor authentication (MFA). Even if the attacker has the correct username and password, they cannot complete login without the second factor. Breach password detection and bot detection are effective supplements.

Question

What is k-anonymity in breach password checking?

Answer

A privacy-preserving technique where only the first 5 characters of the password's SHA-1 hash are sent to the API. The API returns all hashes with that prefix, and the client checks locally. The full password is never transmitted.

Question

Why use a dummy hash comparison for non-existent usernames?

Answer

To prevent username enumeration. Without it, login attempts for non-existent usernames return immediately, while existing usernames take longer (hash computation). The timing difference reveals valid usernames.

Revision Notes

Key Takeaways

  • 1. Brute-force targets one account with many passwords; credential stuffing targets many accounts with leaked credentials
  • 2. Rate limiting is effective against brute-force but insufficient against distributed credential stuffing
  • 3. Progressive delays (exponential backoff) are preferred over hard lockouts to prevent denial-of-service
  • 4. MFA is the most effective defense against credential stuffing
  • 5. Use dummy hash comparison for non-existent usernames to prevent username enumeration

Interview Tips

  • • Explain the difference between brute-force and credential stuffing with concrete examples of each
  • • Discuss why progressive delays are superior to hard lockouts — focus on the denial-of-service risk
  • • Describe a multi-layered defense strategy that addresses both attack types
  • • Connect rate limiting to the OWASP Top 10 (A07: Identification and Authentication Failures) and explain how it fits into a comprehensive authentication security strategy

Cheat Sheet

Brute-Force Defenses and Rate Limiting Cheat Sheet

Phase: Password Storage and Session Security | Level: intermediate

Attack Types:

  • Brute-force: one account, many passwords → rate limiting effective
  • Credential stuffing: many accounts, leaked credentials → MFA, breach detection effective

Defense Layers:

  1. Per-account rate limit (5 failed attempts per 15 minutes)
  2. Per-IP rate limit (20 requests per minute)
  3. Progressive delays (exponential backoff: 2s, 4s, 8s, 16s, max 30s)
  4. CAPTCHA after 3 failed attempts
  5. MFA for all accounts
  6. Breach password detection (Have I Been Pwned k-anonymity API)
  7. Bot detection and device fingerprinting

Key Principles:

  • Progressive delays > hard lockouts (prevents denial-of-service)
  • Dummy hash comparison for non-existent usernames (prevents username enumeration)
  • Rate limiting alone is insufficient against distributed credential stuffing

Key points:

  • Per-account rate limits raise the cost of online guessing against a single username
  • Progressive lockouts must avoid becoming a denial-of-service vector against real users
  • Credential stuffing lists from other breaches are blocked by MFA more than by lockouts

Tags: passwords, rate-limiting

Continue Learning

Before You Start

  • No prerequisites listed.