Quick answer
Credential stuffing is an attack that takes username and password pairs leaked from one service and tries them against another, betting that people reuse passwords. It is not password guessing: the attacker already has real credentials and is only testing where else they work. Because each individual attempt looks like an ordinary login and the success rate only needs to be a fraction of a percent to be profitable, it is far harder to spot than classic brute force.
01How it differs from brute force and password spraying
OWASP catalogues credential stuffing as automated threat OAT-008 and defines it as mass login attempts used to verify the validity of stolen username and password pairs. The attacker is not guessing. They already hold real credentials and are only testing where else they work. In the OWASP Top 10, credential stuffing falls under A07, Authentication Failures.
That makes it a close relative of two other login attacks, and the differences decide which defences work. The last row of the table is why standard defences miss it: locking an account after five failed attempts does nothing when each account is tried exactly once.
| Brute force | Password spraying | Credential stuffing | |
|---|---|---|---|
| Input | Guessed or generated passwords | A few very common passwords | Real pairs from earlier breaches |
| Target | Usually one account | Many accounts | Many accounts |
| Attempts per account | High | One per password tried | Often a single try |
| Success rate | Very low | Low | 0.1 to 2% in Shape Security's research: low, but profitable at scale |
| Caught by account lockouts | Usually | Rarely | Rarely, since each account is tried once |
02How the attack runs
The attacker starts with a combo list: millions of pairs from previous breaches, freely traded. They point account-checking tools at your login endpoint and work through the list, usually spreading it across residential proxies or a botnet so that no single address stands out.
Successful pairs are collected and either used directly, resold, or used for fraud on stored payment methods. Because the credentials are correct, nothing in your application logs looks unusual: a successful login is a successful login.
Password reuse is what makes it pay. OWASP cites the 2011 Sony breach, where two-thirds of the users who also appeared in the earlier Gawker breach had used the same password for both.
03What actually detects it
The tell is never in one request. It is in the aggregate shape of the traffic, so measure ratios rather than counts: the share of login attempts that fail, compared with that endpoint's own normal level.
- A failure rate on the login endpoint far above its normal baseline
- Many distinct usernames from one address, network or fingerprint
- Requests that hit only the login endpoint, with none of the page loads a real session produces
- TLS or HTTP fingerprints that belong to tooling rather than browsers
- Logins arriving faster than a person could type
- A sudden spread of source addresses concentrated on one endpoint
# combined log format: field 6 is the method, 7 the path, 9 the status
awk '$6 == "\"POST" && $7 == "/login" && $9 == 401 {
split($4, t, ":"); print t[2] ":" t[3]
}' access.log | sort | uniq -c
# many apps answer a failed login with 200 and an error page:
# log the outcome in the application instead and count that04Defending against it
There is no single control that solves this, but the combination is very effective. The goal is to make the economics fail, since the attacker needs volume to profit from a sub-one-percent success rate.
- Rate limit the login endpoint specifically, keyed on more than the IP address: per account, per network and per client as well
- Put a proof-of-work challenge or a similar check in front of the password check, so tooling pays a cost per attempt
- Offer and encourage multi-factor authentication; OWASP cites Microsoft's finding that it would have prevented 99.9% of account compromises
- Check new passwords against known-breached lists at sign-up and change, as NIST SP 800-63B requires
- Alert on the endpoint's failure rate rather than on individual failures
- Never reveal whether the username or the password was wrong
05What the standards require
NIST's digital identity guidelines, SP 800-63B revision 4 (August 2025), say verifiers shall compare a prospective password against a blocklist of commonly used, expected or compromised values, explicitly including passwords from previous breach corpuses.
The same document requires verifiers to limit consecutive failed attempts on a single account to no more than 100. That cap is aimed at guessing; it does nothing against an attacker who tries each account once, which is why a per-account cap and endpoint-wide detection belong together.
Checking passwords against breach data does not mean sending them anywhere. The Pwned Passwords range API uses k-anonymity: you send the first five characters of the password's SHA-1 hash and compare the returned suffixes locally. It needs no API key, and an Add-Padding header makes every response a similar size.
read -rs -p 'Password: ' pw; echo
hash=$(printf '%s' "$pw" | sha1sum | awk '{print toupper($1)}')
curl -s -H 'Add-Padding: true' "https://api.pwnedpasswords.com/range/${hash:0:5}" \
| grep -i "^${hash:5}:" || echo 'not found in Pwned Passwords'06A per-address limit on your own server
If you run nginx in front of your application, a dedicated limit on the login route is a cheap first layer. The values below are placeholders to tune against your own traffic, not a recommendation, and limit_req_status 429 replaces nginx's default 503 so clients see a rate limit rather than an outage; adding Retry-After is shown in how site owners should send a 429.
Two caveats apply. Behind a reverse proxy or CDN, restore the visitor's address with the real IP module first, or every visitor shares the proxy's bucket and one attacker locks everyone out. And a per-address limit cannot stop an attack spread across thousands of addresses on its own; it removes the crudest tooling so that the other controls see a cleaner signal.
# http context
limit_req_zone $binary_remote_addr zone=login:10m rate=10r/m;
# inside your existing server block
location = /login {
limit_req zone=login burst=5 nodelay;
limit_req_status 429;
proxy_pass http://app; # your existing upstream
}07If you are being targeted right now
- Confirm it: compare the login failure ratio and the number of distinct usernames per source with a normal day
- Protect the endpoint first: put a challenge in front of the login and tighten its rate limits before anything else
- Find the accounts that were opened: successful logins from the same sources or fingerprints during the attack window
- Reset those sessions and passwords, and tell the affected users what happened and what to do
- On WordPress, cover xmlrpc.php as well as the login form, as described in protecting WordPress with a WAF
- Afterwards, add breached-password checks and make MFA easy to turn on, so the next list finds less
Questions
Frequently asked questions
Sources
Sources and verification
nginx snippet validated with nginx -t (nginx 1.22.1); the Pwned Passwords lookup run against the live range API; the awk query run with GNU awk 5.2 against a sample access log; Debian 12, October 2026.
- 01OAT-008 Credential Stuffing · OWASP
- 02Credential stuffing · OWASP
- 03Credential Stuffing Prevention Cheat Sheet · OWASP
- 04SP 800-63B: Authentication and Authenticator Management (revision 4) · NIST
- 05Have I Been Pwned API v3: Pwned Passwords · Have I Been Pwned
- 06Credential-stuffers enjoy up to 2% attack success rate (Shape Security report) · The Register
Last updated