| name | prevent-credential-stuffing |
| description | Use when building login endpoints — detecting and blocking automated credential stuffing attacks using breached password detection, device fingerprinting, and bot detection. |
| source | OWASP Credential Stuffing Prevention Cheat Sheet (owasp.org/www-project-cheat-sheets); NIST SP 800-63B Section 5.1.1.2; HaveIBeenPwned API documentation; Shape Security research |
| tags | ["security","owasp","credential-stuffing","authentication","bot-detection","rate-limiting","developer"] |
Prevent Credential Stuffing
Detect and block credential stuffing attacks by checking submitted passwords against breached credential databases, implementing multi-signal bot detection, and using device fingerprinting — distinguishing automated attacks from legitimate users.
Why This Is Best Practice
Adopted by: OWASP Credential Stuffing Prevention Cheat Sheet (2023) is the primary reference. NIST SP 800-63B Section 5.1.1.2 mandates checking new passwords against lists of commonly used, expected, or compromised values. HaveIBeenPwned (HIBP) API is used by Microsoft, 1Password, Google Chrome, and Mozilla Firefox for real-time breached password checking. Shape Security (now F5) research documents credential stuffing as responsible for 90% of login traffic on major e-commerce sites.
Impact: Verizon DBIR 2023 attributes 49% of breaches to stolen credentials — credential stuffing is the primary mechanism for exploiting those credentials at scale. The 2022 Rockstar Games breach used credential stuffing to access employee accounts. The 2021 Coinbase credential stuffing attack compromised 6,000+ accounts by exploiting weak SMS MFA. Shape Security estimates credential stuffing attacks generate 2–3 billion login attempts per day across the internet. A site with 1 million users and 1% password reuse rate has 10,000 accounts vulnerable to stuffing from any major data breach.
Why best: IP-based rate limiting alone fails because attackers use botnets with thousands of IPs (e.g., 1 attempt per IP per day is below most limits but allows millions of attempts). Multi-signal detection (failed login velocity, device fingerprint novelty, IP reputation, user-agent analysis) distinguishes automated attacks without degrading legitimate user experience. Breached password detection prevents accounts from using compromised credentials in the first place.
Sources: OWASP Credential Stuffing Prevention Cheat Sheet; NIST SP 800-63B Section 5.1.1.2; Shape Security "State of Credential Stuffing" (2022); HaveIBeenPwned API documentation
Steps
-
Check passwords against HaveIBeenPwned on login and registration:
import hashlib
import httpx
def is_password_breached(password: str) -> bool:
"""
HIBP k-anonymity API: send first 5 chars of SHA-1 hash.
Server returns all hashes with that prefix. Never sends full hash.
"""
sha1 = hashlib.sha1(password.encode()).hexdigest().upper()
prefix = sha1[:5]
suffix = sha1[5:]
response = httpx.get(
f"https://api.pwnedpasswords.com/range/{prefix}",
timeout=3.0,
headers={"Add-Padding": "true"},
)
response.raise_for_status()
for line in response.text.splitlines():
hash_suffix, count = line.split(":")
if hash_suffix == suffix:
return True
return False
def check_at_login(email: str, password: str) -> dict:
user = authenticate(email, password)
if not user:
return {"success": False}
if is_password_breached(password):
return {
"success": True,
: ,
:
}
{: }
Rules
- Never block logins solely based on IP — botnets have millions of IPs; per-IP limits slow bots but don't stop them; combine with account-level and global signals.
- Breached password checks must use k-anonymity (HIBP range API) — sending full password hashes to a third party would create a credential database.
- Response timing must be identical for valid accounts and invalid accounts — timing differences allow username enumeration that feeds credential stuffing lists.
- Account lockout must use exponential backoff, not permanent lockout — permanent lockout enables DoS against a specific user.
Common Mistakes
- Returning different HTTP status codes for valid vs invalid usernames — 404 for missing user vs 401 for wrong password enables account enumeration; always return 401.
- IP allowlisting that bypasses MFA or rate limits — corporate NAT IPs may appear in blocklists; investigate before permanent allowlisting.
- Not logging failed login attempts with IP and user-agent — without this data, credential stuffing campaigns are invisible until account takeovers are reported.
- CAPTCHA as the only defense — CAPTCHA solving services ($1–2 per 1000 solves) make CAPTCHA-only defenses economically viable to bypass; use CAPTCHA as one layer among many.