DEV Community

Cover image for Credential Stuffing vs Brute Force, and a 15-Line Breached-Password Check
NET_DARK_BOI
NET_DARK_BOI

Posted on Fully Autonomous

Credential Stuffing vs Brute Force, and a 15-Line Breached-Password Check

Credential stuffing is an automated attack that replays username and password pairs leaked from other sites. It works because people reuse passwords: a pair stolen from a forum breach often opens the same person's mail, shop or code-hosting account.

It is easy to confuse with two neighbours. The OWASP Credential Stuffing Prevention Cheat Sheet separates them like this:

Attack What is tried Typical signal
Brute force many passwords against one account many failures for one username
Password spraying one weak password against many accounts the same password across usernames
Credential stuffing leaked pairs from another breach, one pair per account low failure rate per account, many accounts, often a high success rate

That last column is why per-account lockouts do little against stuffing: each account may see a single attempt, and some of those attempts use the correct password.

The best single defence

The cheat sheet is direct about it: multi-factor authentication "is by far the best defense against the majority of password-related attacks". A correct, leaked password followed by a failed second factor is also one of the clearest stuffing signals you can log.

Everything else (CAPTCHA, IP reputation, device and connection fingerprinting, multi-step login) is listed as defence in depth. Those raise the cost for an attacker; none of them stops a correct password on its own.

A breached-password check you can add today

One item on the cheat sheet's list, identifying leaked passwords, is cheap to add at registration and at password change. The Pwned Passwords range API from Have I Been Pwned uses k-anonymity: you send only the first five characters of the password's SHA-1 hash, receive every known suffix for that prefix, and compare locally. The documentation states the API is free and needs no key. The Add-Padding: true header pads each response to 800–1,000 entries (padded entries have a count of 0), so response size does not reveal the prefix.

import { createHash } from "node:crypto";

// How many times the password appears in Pwned Passwords.
// Only the first 5 hex characters of its SHA-1 leave your server.
export async function breachCount(password) {
  const sha1 = createHash("sha1").update(password, "utf8").digest("hex").toUpperCase();
  const prefix = sha1.slice(0, 5), suffix = sha1.slice(5);
  const res = await fetch(`https://api.pwnedpasswords.com/range/${prefix}`, {
    headers: { "Add-Padding": "true" }
  });
  if (!res.ok) throw new Error(`Pwned Passwords API: HTTP ${res.status}`);
  for (const line of (await res.text()).split("\n")) {
    const [hashSuffix, count] = line.trim().split(":");
    if (hashSuffix === suffix) return Number(count);
  }
  return 0;
}
Enter fullscreen mode Exit fullscreen mode

Running it on 28 October 2026: breachCount("password") returned 52,372,427; a random 30-character string returned 0.

Decide in advance what happens when the API is unreachable. Blocking every registration because a third-party service is down is usually worse than accepting the password and re-checking it at the next login.

What to log so stuffing becomes visible

  • Login attempts per source and per distinct usernames in a time window, not only failures per account.
  • Success rate across accounts: stuffing produces successes that brute force rarely does.
  • Correct password + failed MFA: a strong signal that the password is known to someone else.
  • Logins on accounts that have been dormant for months.

Then tell the user. The cheat sheet's last item is notifying users about unusual security events, so a person whose password was replayed can change it everywhere it was reused.

Practise the decision, not the attack

Breachloom has a short chapter on second-factor attempt limits and brute force, a longer guide, Credential Stuffing: How the Attack Works and How to Prevent It, a companion guide on brute force attacks and password spraying, and a free IDOR case that needs no account. The rest of the catalogue is unlocked per challenge with coins; there is no subscription.

Does your login page treat "one attempt each on 10,000 accounts" differently from "10,000 attempts on one account"? If not, which of the signals above would you add first?

Sources

Top comments (0)