Skip to content
Innovialabs

Credential Stuffing and Account Takeover: Detection and Defense

Advanced4 min

In short

Credential stuffing replays username-password pairs leaked from other sites; stop it with layered defenses — block breached passwords via k-anonymity checks, score each login by device and behavior, ask for a second factor only when risk is high, filter bots and alert on login anomalies.

How credential stuffing works

People reuse passwords. When one site leaks its user database, attackers take the email-password pairs and replay them automatically against other services: banks, shops, delivery apps, SaaS products. Every pair that still works becomes an account takeover (ATO): stolen bonuses, saved cards, personal data or a foothold for fraud.

What makes it hard to stop:

  • requests come from residential proxies and thousands of IPs, so per-IP limits barely trigger;
  • each account receives only one or two attempts, so per-account lockouts do not trigger either;
  • bots imitate real browsers, solve simple challenges and rotate user agents;
  • the attacker's credentials are correct — a successful login looks completely legitimate.

Rate limiting is still necessary, but it is the first layer, not the defense.

Layer 1. Reject breached passwords

If a password is already in public breach corpora, it will be tried. Check passwords at registration, password change and login, as recommended by NIST SP 800-63B.

The Pwned Passwords service uses k-anonymity: you hash the password with SHA-1, send only the first 5 hex characters, receive all suffixes with that prefix and compare locally. The full password and full hash never leave your server.

import hashlib
import requests

def is_breached(password: str) -> bool:
    digest = hashlib.sha1(password.encode("utf-8")).hexdigest().upper()
    prefix, suffix = digest[:5], digest[5:]
    resp = requests.get(f"https://api.pwnedpasswords.com/range/{prefix}", timeout=5)
    resp.raise_for_status()
    return any(line.split(":")[0] == suffix for line in resp.text.splitlines())

On a match at login, let the user in only after an extra check and force a password change. Decide in advance what happens if the service is unavailable: usually fail open for login, but log it.

Layer 2. Score every login by risk

Collect signals and combine them into a risk score instead of a single yes/no rule:

  • Device: is this a known device or browser for this account? A stored device token or cookie is a strong positive signal.
  • Network: datacenter or hosting ASN, known proxy or Tor exit, a country the user has never logged in from.
  • Behavior: time between page load and submit, typing and pointer patterns, direct POST to the login API without loading the page.
  • History: impossible travel between two logins, several accounts logging in from one device, a password reset right after a new-device login.

Keep signals server-side where possible; anything the client sends can be forged.

Layer 3. Step-up authentication

Use the score to decide what to ask for:

RiskAction
LowLog in normally
MediumAsk for a one-time code or email confirmation
HighRequire a strong second factor or block and notify the user

Passkeys and app-based codes resist credential stuffing well because a reused password alone is no longer enough. SMS codes are better than nothing but weaker against SIM swapping. Offering MFA to everyone while challenging only risky logins keeps friction low.

Layer 4. Detect and slow down bots

  • Put the login endpoint behind bot management at the edge or a WAF with credential-stuffing rules.
  • Show a challenge only when signals are suspicious, not on every login.
  • Return the same error and similar response time for a wrong password and a nonexistent user, so attackers cannot validate their lists.
  • Apply limits across several dimensions at once: IP, subnet, ASN, device, account and globally.
  • Protect every entry point: mobile API, password reset, legacy endpoints and partner logins are often forgotten.

Layer 5. Monitor login anomalies

Credential stuffing has a recognizable shape in metrics. Alert on:

  • a spike in total login attempts or in the failure ratio;
  • a growing share of "user not found" errors — the attacker's list includes emails you never had;
  • many accounts tried from one device fingerprint;
  • a rise in successful logins from new devices followed by changes to email, phone or payout details.

When an attack is confirmed: tighten thresholds, invalidate sessions of affected accounts, force password resets, and notify users about new-device logins so they can react themselves.

Common mistakes

  • Relying on one control, usually a CAPTCHA, which bots can outsource.
  • Locking accounts after a few failures — this hands attackers an easy way to lock out real users.
  • Telling the user "this email is not registered" on the login form.
  • Protecting the web login but leaving the mobile API open.

FAQ

No. A long, complex password that was reused and leaked elsewhere is just as useful to an attacker. Breached-password checks and a second factor matter more than complexity rules.

We can help with this

If you'd rather get the result than figure it all out yourself — this is what we do every day.

Discuss a project

Read next

Have an idea or a task? Tell us about it

We'll reply within one business day and propose a solution. The consultation is free.