Skip to content

Brute force and credential stuffing

CWE-307OWASP A07:2021Updated August 31, 20267 min read

Brute force is the systematic trying of credentials until one combination works; credential stuffing replays username and password pairs that leaked from another company. Both only succeed when your application puts no limit on the number of attempts. The fix is throttling per account and per source, a mandatory second factor, and a check against breached passwords.

Every login form reachable from the internet gets a daily visit from automated scripts. They guess passwords, or they submit email addresses and passwords that leaked from some other company. As long as your application accepts an unlimited number of attempts, getting in is mostly a question of time and bandwidth. Here is how those attacks work and which controls make them uneconomical.

What are brute force and credential stuffing?

Brute force is an attack in which someone systematically tries credentials until one combination works, because the application puts no limit on the number of attempts. Credential stuffing is the more effective variant: the attacker does not guess, but submits email and password pairs stolen from another company, knowing that a share of your users reuse the same password everywhere. In the CWE catalogue the underlying weakness is called improper restriction of excessive authentication attempts, CWE-307.

An everyday comparison: a burglar who tries every key on a large ring against your front door is running a brute force attack. A burglar who found the key to your bike shed and tries it on your front door without further thought is doing credential stuffing. The second one needs far fewer attempts, because they start with a key that demonstrably belongs to you.

In between sits password spraying. The attacker picks one common password, a season followed by the year for example, and tries it against thousands of accounts at once. Each account sees a single attempt, so a counter that only looks at one account notices nothing unusual. That is exactly why a lockout after five failed attempts on one account is no defence against this variant.

When a second factor does stand in the way, the attack moves one step further. MFA stands for multi-factor authentication: besides the password, a second proof is required, such as a code or a hardware key. In an MFA fatigue attack the attacker already holds the password and sends push notifications until the user approves one out of habit or irritation.

How does a brute force or credential stuffing attack work?

Take an ordinary login form. The application looks the user up, compares the password and sets a session on success. There is no counter, no delay and no limit per source.

Vulnerable:

app.post("/login", async (req, res) => {
  const { email, password } = req.body;

  const user = await db.users.findByEmail(email);
  if (!user) {
    // Two different messages reveal which addresses exist
    return res.status(401).send("Unknown email address");
  }

  if (await bcrypt.compare(password, user.passwordHash)) {
    req.session.userId = user.id;
    return res.redirect("/dashboard");
  }

  // No counter, no delay, no limit per source
  return res.status(401).send("Wrong password");
});

The attacker captures one successful request from their own browser and replays it with a list. For credential stuffing that list comes from a public collection of leaked pairs; for spraying it is one password against many addresses.

POST /login HTTP/1.1
Host: app.example.com
Content-Type: application/x-www-form-urlencoded

email=a.smith@example.com&password=Summer2026!

A request like that costs a few milliseconds. With a thousand concurrent connections through residential proxies, hundreds of thousands of combinations per hour are within reach, spread over so many IP addresses that no single one stands out. The two distinct error messages help the attacker along: “Unknown email address” confirms which addresses do not exist, so they clean up the list first and only then start trying passwords.

The secure version does four things at once. It counts attempts per account and per source, it returns the same error message in both cases, it spends the same amount of computation on an unknown address as on a real one, and it treats the password as the first of two steps.

Secure:

const MAX_PER_ACCOUNT = 5;   // per 15 minutes
const MAX_PER_SOURCE = 50;   // per 15 minutes, per IP address

app.post("/login", async (req, res) => {
  const { email, password } = req.body;
  const account = "acct:" + normaliseEmail(email);
  const source = "src:" + clientIp(req);

  // Two independent counters, held in a shared store
  if (await overLimit(account, MAX_PER_ACCOUNT, 900) ||
      await overLimit(source, MAX_PER_SOURCE, 900)) {
    return res.status(429).send("Too many attempts. Please try again later.");
  }

  const user = await db.users.findByEmail(email);

  // Always hash, even without a matching user: equal response time
  const hash = user ? user.passwordHash : DUMMY_HASH;
  const ok = (await bcrypt.compare(password, hash)) && Boolean(user);

  if (!ok) {
    await countFailure(account, source);
    // One message covering both cases
    return res.status(401).send("Email address or password is incorrect.");
  }

  await resetFailures(account);

  // The password checks out; identity is only settled after the second factor
  req.session.pendingUserId = user.id;
  return res.redirect("/login/mfa");
});

The per-account counter slows down guessing against a single victim. The per-source counter slows down spraying, which deliberately spreads its attempts across many accounts and stays invisible per account. You need both: each counter covers precisely the attack the other one lets through. Keep the counters in a central store such as Redis, otherwise every application server counts its own share and the real limit is a multiple of the one you configured.

A second factor is the strongest control against credential stuffing, but not every factor is equal. A push notification that only asks for approve or deny invites MFA fatigue: the attacker who already holds the password sends dozens of prompts in a row until someone taps approve. That is how the 2022 breach at Uber began. Use number matching, show the location and application in the prompt, and cap the number of prompts per minute.

What is the impact of brute force and credential stuffing?

A single taken-over account gives the attacker everything that user may see and do: personal data, orders, invoices, internal documents. A second stage usually follows from there, because messages sent from inside are rarely questioned. If the account belongs to an administrator or a service desk agent who can reset passwords, the damage reaches the whole platform.

In business terms that means fraud and chargebacks, abuse of stored payment details, theft of loyalty points or account balances, and a possible notification duty to the regulator once personal data has been accessed. The attack itself also costs capacity: a large credential stuffing wave can flatten authentication servers and help desks even when not a single attempt succeeds.

Because severity depends on which account falls and on what sits behind it, the classification runs from medium to high. One customer account holding limited data, protected by mandatory MFA, stays manageable. A system with no second factor, where the same password also opens the VPN or the mailbox, sits at the top of that range.

How do you detect brute force and credential stuffing?

In the logs the pattern is recognisable: a ratio of failed to successful logins that suddenly flips, many different usernames from one source, or conversely a single password attempt per account spread over hundreds of sources. Watch as well for uniform user agents, unusual hours, and a spike in password resets shortly after a known breach elsewhere.

A tester starts by asking whether there is any limit at all, and then whether that limit can be bypassed. The classic gaps: the counter is tied to the session cookie and disappears the moment you drop it, the application trusts the X-Forwarded-For header and the attacker fills it in themselves, or the web form is throttled while /api/login, the mobile API and a legacy protocol such as IMAP run unrestricted. Do not forget the surrounding endpoints: forgotten password, one-time code verification and registration often reveal whether an address exists, and a six-digit OTP with no limit can be exhausted in minutes.

Scanners only observe whether a burst of fast attempts on the main route gets blocked. Whether the limit also holds against a distributed, slow attack and on every side route only becomes clear through manual work. AssistSec covers these checks in a penetration test, including the behaviour of the second factor and the error messages around usernames.

How do you prevent brute force and credential stuffing?

  • Limit attempts per account and per source. Use an increasing delay and a temporary block rather than a hard lockout, and keep the counters in a shared store so they apply across all servers.
  • Turn MFA on by default, not only for administrators. Prefer passkeys or a hardware key; if you use push notifications, enable number matching and cap the number of prompts.
  • Check new passwords against known breaches. Compare locally against a list of leaked hashes, or use a range query on the first five characters of the hash so the password itself never leaves your environment.
  • Return the same error message and the same response time. Login, password recovery and registration should never reveal whether an email address is known.
  • Protect every entrance. APIs, mobile clients, legacy protocols, password recovery and one-time code verification need the same limits as the web form.
  • Log and alert on patterns, not on single attempts. Measure the ratio of failed to successful logins per minute, and notify the user of a sign-in from a new device or location.
  • Add a second hurdle for suspicious traffic. A CAPTCHA or a proof of work after a number of failed attempts hurts automated traffic badly and ordinary users hardly at all.

Sources

Frequently asked questions

What is the difference between brute force and credential stuffing?

In a brute force attack the attacker guesses: they work through a wordlist or a generated character set until a password fits. In credential stuffing they do not guess at all, but replay complete username and password pairs that leaked from another company. Credential stuffing therefore needs far fewer attempts and succeeds more often, because many people reuse the same password in several places.

Does locking an account after five failed attempts help?

Only partly, and it can be turned against you. A per-account lockout stops guessing against one victim, but not password spraying, where the attacker tries one password across thousands of accounts and stays under the limit on each. Anyone with a list of email addresses can also lock out your entire user base. Slowing requests down, blocking temporarily and counting per source as well works better than a hard lock.

Does MFA stop credential stuffing completely?

MFA is by far the strongest control, but it is not absolute. An attacker who already holds the password can try to get past the second factor through MFA fatigue, through phishing of a one-time code, or through an endpoint that never rate limits the code itself. Throttle the verification step as well, and prefer phishing-resistant factors such as passkeys or a hardware key.

How do I check whether a password appears in a breach?

You can do it without sending the password anywhere. Hash it locally with SHA-1, send only the first five characters of that hash to a service holding known breached hashes, and compare the returned suffixes yourself. This range method is how Have I Been Pwned exposes its password list. Run the check at registration and on every password change.

Related articles

Press / to search · Esc