Skip to content

Search ThreatNotes

Anatomy of a Credential Stuffing Attack

Credential stuffing is not brute force. Understanding the difference is what makes it detectable — and what makes most rate limits useless against it.

ThreatNotes7 min read

Credential stuffing gets lumped in with brute force, and that framing is why so many teams defend against it badly. Brute force guesses passwords. Credential stuffing doesn’t guess anything — it replays credentials that are already known to be valid somewhere else. The attacker isn’t attacking your password policy. They’re attacking the fact that your users reuse passwords.

That distinction changes what the traffic looks like, and therefore what detection has to look for.

How a campaign actually unfolds

Five stages, and only the third one touches your login endpoint. Step through them before reading on — most of the defensive mistakes below come from treating stage three as the whole attack.

A credential stuffing campaign, stage by stage
  1. Step 1 of 5

    Collect the credentials

    Nothing is guessed. Username and password pairs are aggregated from years of unrelated breaches into combolists, deduplicated, and traded in bulk. A single list can run to hundreds of millions of pairs.

  2. Step 2 of 5

    Rent the infrastructure

    The attempts are pushed through residential proxy networks, so each request arrives from an ordinary consumer address. The traffic looks like your real users because, at the network layer, it is coming from real users’ connections.

  3. Step 3 of 5

    Spray, one attempt per account

    Each account sees a single login attempt, and each source address is used sparingly. No per-account counter and no per-IP counter is ever tripped, because the attacker has more accounts and more addresses than you have thresholds.

  4. Step 4 of 5

    A fraction land

    At a realistic reuse rate of around 0.1%, a million attempts yields roughly a thousand working accounts. The attacker does not need to be efficient — the raw material was effectively free, so a tiny hit rate is still profitable.

  5. Step 5 of 5

    Cash out

    Working accounts are drained, resold, or used as a foothold — stored balances, loyalty points, saved payment methods, or the mailbox that resets every other password. This is the stage your fraud team notices, days after the login traffic that caused it.

Where the credentials come from

The raw material is combolists: username and password pairs aggregated from years of unrelated breaches, deduplicated and traded in bulk. A single list can run to hundreds of millions of pairs. They are cheap, and for the attacker they are effectively free — the cost of the operation is infrastructure, not acquisition.

The economics matter. If a list yields a 0.1% success rate against your login endpoint — which is a realistic figure for consumer services — then a million attempts produces around a thousand working accounts. The attacker doesn’t need to be efficient. They need to be patient and distributed.

What the traffic actually looks like

Here’s the shape that trips people up. A naive brute-force attack is:

198.51.100.7  ->  POST /login  user=alice  (attempt 1)
198.51.100.7  ->  POST /login  user=alice  (attempt 2)
198.51.100.7  ->  POST /login  user=alice  (attempt 3)

Many attempts, one account, one source. Every rate limiter ever written catches this.

Credential stuffing is closer to:

203.0.113.14  ->  POST /login  user=alice    (attempt 1)
198.51.100.9  ->  POST /login  user=bob      (attempt 1)
192.0.2.55    ->  POST /login  user=carol    (attempt 1)
203.0.113.88  ->  POST /login  user=dave     (attempt 1)

One attempt per account. One attempt per source address. Nothing exceeds a per-account or per-IP threshold, because the attacker has more accounts and more addresses than you have thresholds. Residential proxy networks make the source addresses look like ordinary consumer traffic, because they are ordinary consumer traffic — someone else’s.

Why per-account rate limiting fails

Per-account limits are still worth having; they stop the naive case. But against stuffing they’re the wrong axis entirely. The attacker never touches the same account twice in a window. Locking an account after five failures does nothing when the attack makes one attempt against each of five million accounts.

Worse, aggressive account lockout hands the attacker a denial-of-service primitive. If they can lock an account by failing against it, and they have a list of your usernames, they can lock your entire user base. This has happened to real services.

What actually detects it

The signal is not in any single request. It is in the aggregate.

Global failure ratio. Your login endpoint has a baseline success rate. Legitimate users mostly get their password right. If the ratio of failures to successes across the whole endpoint shifts sharply — even while every individual account and IP stays under its limit — something is replaying credentials. This is the single highest-value signal, and it costs almost nothing to compute.

Distinct-account velocity per source. A residential IP that attempts one login against forty different accounts in an hour is not a household. Pivot the rate limit from “attempts per account” to “distinct accounts per source.”

Impossible client populations. Stuffing tools have fingerprints. A sudden concentration of one exact User-Agent, one TLS fingerprint, or a client population that never requests your CSS is worth alerting on. Attackers do rotate these, but rotation itself is anomalous.

Success clustering. The successful logins are the ones that matter. If accounts that just succeeded share no history with the device or region logging in, treat the success as suspicious rather than treating the failures as suspicious. Failures are noise; unexpected successes are the breach.

What actually stops it

Detection buys you time. It doesn’t fix the underlying problem, which is that the credentials are genuinely correct.

The structural fix is to make a correct password insufficient. Phishing-resistant MFA — passkeys or hardware tokens — ends credential stuffing outright, because the replayed secret is no longer the whole secret. Push-based MFA helps far less than people assume, for reasons worth their own post.

Short of that:

  • Check credentials against known-breached corpora at set time. Have I Been Pwned’s range API lets you do this without ever sending a password or its full hash. Rejecting known-compromised passwords at registration removes the attacker’s raw material.
  • Step up on risk, not on schedule. Challenge the unusual login, not every login. Friction spent on legitimate users buys nothing.
  • Never reveal whether the username exists. Different responses, or even measurably different response times, for “no such user” versus “wrong password” turn your login endpoint into a free account-enumeration oracle. Attackers use it to prune their lists before they even start.

The one thing to take away

If your login defences are built around counting failures per account, you are instrumented for the wrong attack. Add one metric — the endpoint-wide ratio of failed to successful authentications, tracked over time — and you will see stuffing campaigns you are currently blind to. It is the cheapest detection improvement available on this class of attack.

Liked this? There's one every week.

New write-ups, course drops, and the Tuesday roundup — in a single email. No sponsors, one-click unsubscribe.

See what you’d get

Suggested for you

  • Weekly News

    The software you already trusted

    Two browser extensions, a car alarm fitted at the dealership, and WordPress core itself. Almost nothing that went wrong this week came from outside — it came from things that were already installed and already trusted.

  • Post

    Why MFA Fatigue Attacks Work

    The failure is not that users are careless. It is that push approval asks a question the user has no way to answer correctly — and asks it dozens of times until they slip.

  • Post

    What MFA Actually Protects You Against

    MFA is either “a magic switch that ends hacking” or “already broken, why bother”, depending on who you ask. Both are wrong. Here is the honest version — what it kills stone dead, what walks straight past it, and which kind to actually turn on.

Comments

Reactions and replies are powered by GitHub Discussions. Signing in with GitHub is required to post.