Credential Stuffing Detection
Credential stuffing detection that finds what hides under per-IP thresholds. Success-rate inversion, low-and-slow campaigns, and ATO signals in SPL and KQL.
Credential stuffing detection fails in most organisations for a specific and fixable reason: the detection is built per source IP, and the attack is built to stay under per-source thresholds. A modern stuffing campaign spreads across tens of thousands of residential proxy addresses, makes two or three attempts from each, and looks exactly like a large population of users who mistyped a password. Every individual request is unremarkable. Only the population is anomalous. This guide detects the population.
Credential stuffing maps to MITRE ATT&CK T1110.004 — Brute Force: Credential Stuffing and leads directly to T1078 — Valid Accounts. It is the volume attack against every authentication endpoint on the internet, and the one most often invisible in logs that are technically complete.
What is credential stuffing?
Credential stuffing is the automated replay of username and password pairs stolen from one breach against a different service. It is not brute force and the distinction is operationally important. Brute force guesses; stuffing replays pairs that were already valid somewhere, betting entirely on password reuse.
That bet pays at rates most defenders underestimate. Published success rates cluster somewhere around 0.1 to 2 percent depending on the credential list’s freshness and the target’s demographics. Against a list of ten million pairs, even the low end of that range yields thousands of compromised accounts, and the marginal cost of each attempt is close to zero.
The economics explain the shape of the attack. The attacker is not trying to break one account; they are running a conversion funnel. They do not care about your specific defences so much as about throughput and cost per validated credential, which is why the countermeasure that works is making the attack expensive rather than making any single attempt fail.
| Attack | What varies | What is constant | Detection angle |
|---|---|---|---|
| Brute force | Password | One account | Failures per account |
| Password spray | Account | One common password | One password, many accounts |
| Credential stuffing | Both | Neither | Population-level anomaly |
| Credential cracking | Password offline | The stolen hash | Not visible in your logs |
The third row is the difficult one. Because both the account and the password vary on every attempt, none of the per-entity thresholds that catch the other three have anything to grip. This is why stuffing detection is a fundamentally different problem from password spraying detection and why reusing those analytics produces silence.
What does a credential stuffing campaign look like over time?
Campaigns are not a single undifferentiated flood, and recognising the stage you are looking at changes both the urgency and the correct response. Most run through four phases, each with a different signature.
| Phase | What the attacker does | Duration | Dominant signal | Your best action |
|---|---|---|---|---|
| Reconnaissance | Probes the endpoint, measures responses and timing | Minutes to hours | Account enumeration attempts, odd endpoint paths | Close enumeration oracles |
| Validation probe | Tests a small sample to gauge hit rate | Minutes | Small volume, high nonexistent-account ratio | Fingerprint the tooling early |
| Full run | Works the list at scale across the proxy pool | Hours to days | Success-rate inversion, high distinct-IP count | Graduated friction |
| Monetisation | Logs into validated accounts and acts | Days to weeks | Post-login takeover behaviour | Reset and revoke |
The validation probe is the phase worth investing detection effort in, because it is the cheapest moment to intervene and the one almost nobody catches. An attacker with a fresh list of several million pairs will not spend the proxy budget to run all of it against an unknown target. They test a few hundred first to measure the hit rate, and if the hit rate is poor they move to a different target entirely. That probe is small, brief, and generates nowhere near enough volume to move an aggregate success rate.
What it does generate is a distinctive client fingerprint arriving from a handful of addresses in a tight time window, which is exactly what a fingerprint-uniformity analytic catches. Flag that fingerprint and you can apply friction to the full run before it starts, at the moment your cost of response is lowest and theirs is highest.
The monetisation phase deserves separate emphasis because it frequently arrives long after the authentication phase has ended and been forgotten. Validated credential lists are traded, so the party who ran the stuffing is often not the party who eventually uses the accounts. A campaign you detected and considered closed six weeks ago can produce takeovers now, which is why the post-login behavioural analytic below runs continuously rather than only during an active incident.
What telemetry do you need?
Population-level detection requires fields most authentication logs do not carry, and the gaps are consistent across organisations.
| Requirement | Why it matters | Common failure |
|---|---|---|
| Every attempt logged, success and failure | Success rate is a ratio; you need both terms | Only failures logged |
| Attempted username on failures | Enables one-credential-many-accounts detection | Redacted for privacy |
| True client IP plus resolved ASN | Distinguishes residential proxy pools | Overwritten by the CDN or LB |
| Client fingerprint (TLS/JA3, header order) | Catches uniform tooling across many IPs | Not captured at all |
| Failure reason code | Separates wrong-password from no-such-user | Collapsed into a generic failure |
| Post-login action events | Links stuffing to actual takeover | Auth and app logs not correlated |
| One-minute or finer aggregation | Bursts hide inside hourly rollups | Hourly-only metrics |
Two of these deserve attention. Logging only failures is extremely common and it makes the single best analytic impossible, since a success rate needs a denominator. And collapsing the failure reason destroys a genuinely valuable signal: a wrong password on a real account and an attempt against a nonexistent account mean very different things, and the ratio between them tells you whether the attacker’s list is targeted at your platform or generic.
How to detect credential stuffing
Three analytics: one for the attack in progress, one for the credential list being worked, and one for the takeover that follows.
Success-rate inversion across the source population
The most reliable signal available. Healthy login traffic succeeds most of the time. Stuffing traffic fails almost all of the time, and when it arrives in volume it drags the aggregate ratio down sharply even though no individual source looks unusual.
index=auth sourcetype=app:login_attempt earliest=-60m
| bin _time span=1m
| stats count AS attempts,
sum(eval(result=="success")) AS successes,
dc(src_ip) AS distinct_ips,
dc(username) AS distinct_users,
dc(src_asn) AS distinct_asns
by _time
| eval success_rate = round(successes / attempts * 100, 2)
| eval attempts_per_ip = round(attempts / distinct_ips, 2)
| eventstats avg(success_rate) AS baseline_rate,
stdev(success_rate) AS baseline_stdev
| eval z_score = round((baseline_rate - success_rate) / baseline_stdev, 2)
| where success_rate < 25
AND distinct_ips > 200
AND attempts_per_ip < 5
AND z_score > 3
| table _time, attempts, successes, success_rate, baseline_rate, z_score,
distinct_ips, distinct_users, distinct_asns, attempts_per_ip
| sort _time The attempts_per_ip < 5 clause is what makes this specific to distributed stuffing rather than
to a single broken client hammering the endpoint. Combined with a high distinct-IP count and a
collapsed success rate, it describes exactly one thing. Derive the success_rate < 25 threshold
from your own baseline rather than adopting it directly, since a platform with an unusual login
flow may sit lower normally.
One credential attempted across many accounts
The complementary view. Where success-rate inversion catches volume, this catches the attacker methodically working a list, including the low-and-slow campaigns that never generate enough volume to move an aggregate ratio.
let lookback = 7d;
let min_accounts = 15;
SigninLogs
| where TimeGenerated > ago(lookback)
| where ResultType != 0
| extend ClientIP = tostring(IPAddress),
ASN = tostring(NetworkLocationDetails),
UA = tostring(UserAgent)
| summarize AttemptedAccounts = dcount(UserPrincipalName),
TotalAttempts = count(),
SourceIPs = dcount(ClientIP),
ASNs = dcount(ASN),
UserAgents = dcount(UA),
Window = max(TimeGenerated) - min(TimeGenerated),
SampleAccounts = make_set(UserPrincipalName, 15)
by CredentialHash = tostring(hash_sha256(strcat(UserPrincipalName, ClientAppUsed)))
| where AttemptedAccounts >= min_accounts
| extend AttemptsPerIP = round(todouble(TotalAttempts) / SourceIPs, 2),
AccountsPerHour = round(todouble(AttemptedAccounts) /
(Window / 1h), 2)
| where AttemptsPerIP < 5
| project CredentialHash, AttemptedAccounts, TotalAttempts, SourceIPs, ASNs,
UserAgents, AttemptsPerIP, AccountsPerHour, Window, SampleAccounts
| order by AttemptedAccounts desc The seven-day lookback is deliberate and it is the point of this rule. A low-and-slow campaign
touching fifteen accounts over five days generates nothing on any hourly window and is plainly
visible here. Note the UserAgents count: a genuine population of forgetful users produces
dozens of distinct user-agent strings, while tooling frequently produces one or two across
thousands of source addresses. That uniformity across otherwise-diverse IPs is one of the
hardest properties for an attacker to fix cheaply.
Post-authentication account takeover behaviour
Stuffing that succeeds becomes account takeover, and takeover has its own behavioural signature that is often clearer than the authentication signal that preceded it.
index=auth sourcetype=app:login_attempt result=success earliest=-24h
| join type=inner user_id
[ search index=app sourcetype=app:account_action earliest=-24h
| search action IN ("email_change","password_change","mfa_enroll",
"mfa_remove","api_key_create","payout_change",
"recovery_email_change","session_export")
| fields user_id, action, _time AS action_time, src_ip AS action_ip ]
| eval seconds_to_action = action_time - _time
| where seconds_to_action >= 0 AND seconds_to_action < 900
| lookup user_baseline user_id OUTPUT usual_asn, usual_country, account_age_days
| eval risk = 0
| eval risk = risk + if(src_asn != usual_asn, 2, 0)
| eval risk = risk + if(src_country != usual_country, 2, 0)
| eval risk = risk + if(action IN ("mfa_remove","payout_change","api_key_create"), 3, 0)
| eval risk = risk + if(seconds_to_action < 120, 2, 0)
| eval risk = risk + if(account_age_days > 180 AND src_country != usual_country, 1, 0)
| where risk >= 5
| table _time, user_id, action, seconds_to_action, src_ip, src_asn,
src_country, usual_country, risk
| sort - risk The fifteen-minute window matters. A legitimate user who logs in and changes their email does it
eventually; an attacker validating and monetising a compromised account does it immediately,
because they are working through a list. Scoring rather than single-signal alerting keeps this
usable, and mfa_remove alongside a country change is close to definitive.
Which false positives will you actually see?
| False positive | Which rule it hits | Why it happens | Resolution |
|---|---|---|---|
| Corporate NAT egress | Success-rate inversion | Many users behind one IP | Exclude known corporate ASNs from per-IP maths |
| Mobile app token refresh storm | Success-rate inversion | App bug retries auth in a loop | Separate interactive from programmatic auth |
| Password expiry wave | Success-rate inversion | Policy forces mass resets | Correlate with policy events, suppress the window |
| Shared family accounts | ATO behaviour | Genuinely multiple locations | Baseline per account, not per population |
| Traveling users | ATO behaviour | Real country change | Require a second risk signal, not geography alone |
| SSO misconfiguration | One-credential-many | A broken integration retries widely | Exclude service principals |
| Security scanner | All three | Authorized testing | Allowlist scanner ranges and coordinate windows |
| Post-breach forced reset | Success-rate inversion | Your own remediation | Suppress during declared reset campaigns |
The mobile-app row is the one that most often causes teams to disable the detection. A client bug that retries authentication aggressively looks statistically similar to an attack, and the fix is to separate interactive login attempts from programmatic token refresh at the log level, not to raise the threshold until both disappear.
How do you triage a credential stuffing campaign?
- Confirm it is stuffing, not spraying. Check whether the attempted passwords vary. If one password dominates, it is spraying and the response differs.
- Measure the validated set. Identify which accounts succeeded. That list is the actual incident; the failures are noise around it.
- Force reset the succeeded accounts and revoke their sessions, following the session revocation path so a stolen session does not survive the password change.
- Check each succeeded account for takeover actions in the fifteen minutes after login, using the analytic above. Persistence beats the password reset otherwise.
- Characterise the source population. ASN distribution and fingerprint uniformity tell you whether this is a commodity proxy pool or something more targeted.
- Determine list provenance if you can. A high nonexistent-account ratio means a generic breach dump; a low one means the list was built against you and there may be an earlier incident you missed.
- Deploy graduated friction rather than a block. Blocking the pool invites rotation. Friction raises cost without telling the attacker precisely what tripped.
- Watch the recovery flows. Blocked at login, attackers move to password reset and MFA recovery. That is the next surface, and it is usually weaker.
Step 8 is the most-missed. A well-defended login endpoint routinely displaces the attack onto an account-recovery flow that was designed for user convenience and never threat-modelled.
How to test your credential stuffing detection
Against a lab environment you own or are explicitly authorized to test, using synthetic accounts and synthetic credential pairs only:
- Generate distributed low-volume failed logins across many source addresses. Confirm success-rate inversion fires while per-IP rate limiting stays silent, which demonstrates the gap the analytic exists to close.
- Attempt one synthetic credential across twenty synthetic accounts over several days. Confirm the seven-day KQL rule catches it.
- Successfully authenticate to a synthetic account and immediately remove MFA. Confirm the ATO analytic scores it above threshold.
- Log in from a new country without any sensitive action. Confirm this does not alert, so you know geography alone is not driving the rule.
- Replay a corporate NAT pattern and confirm the exclusion suppresses it.
- Verify the client IP field is the real client and not your edge, which is the failure that silently disables every analytic here.
How to prevent credential stuffing
Four additional controls matter more than their implementation cost suggests:
- Never leak account existence. Identical response bodies and identical timing on “no such user” and “wrong password.” Emit the distinction to your SIEM, never to the client.
- Rate limit on the population, not the source. Adaptive limits keyed on aggregate success rate hold when per-IP limits do not.
- Treat validated-credential discovery as its own incident. Even where MFA blocked takeover, the attacker learned which pairs are valid, and that list has value.
- Monitor for your users’ credentials in breach corpora and force resets proactively.
Which anti-stuffing control should you deploy first?
| Control | Stops | Effort | User friction | Deploy when |
|---|---|---|---|---|
| Breached-credential screening | Reuse at the root | Low | None | Always, first |
| Population-level detection | The campaign itself | Low | None | Always, second |
| Graduated friction / challenge | Throughput | Medium | Low, risk-scaled | You have volume |
| Passkeys / phishing-resistant MFA | Takeover entirely | Medium | Low once enrolled | Always, in parallel |
| Client attestation at edge | Cheap tooling | Medium | None | Bot pressure is sustained |
| Hardened recovery flows | The displaced attack | Medium | Low | You deployed the above |
Breached-credential screening is the highest-leverage item and the most frequently skipped. It is cheap, invisible to legitimate users, and it attacks the precondition the entire attack depends on rather than its symptoms.
Common credential stuffing detection mistakes
- Relying on per-IP rate limiting. It measures the wrong unit for a distributed attack.
- Logging only failed logins. A success rate needs both terms.
- Collapsing the failure reason. You lose the list-provenance signal.
- Alerting on geography alone. Users travel; require a second risk signal.
- Blocking the proxy pool outright. It rotates, and you lose visibility.
- Stopping at the password reset. Check for MFA changes and API keys first.
- Treating MFA as the whole answer. The validated list still has value, and recovery flows become the target.
- Never testing with the client behind a proxy. That is where the client-IP field breaks.
Credential stuffing detection checklist
- Log every authentication attempt with its outcome, not only failures.
- Emit a distinct failure reason for nonexistent account versus wrong password, to the SIEM only.
- Confirm the logged client IP is the real client and not your load balancer or CDN.
- Capture a client fingerprint (TLS or header order) alongside the IP.
- Baseline your normal login success rate at one-minute granularity.
- Alert on aggregate success-rate inversion with a high distinct-IP and low attempts-per-IP shape.
- Run a seven-day one-credential-many-accounts analytic to catch low-and-slow campaigns.
- Correlate successful logins with account actions in the following fifteen minutes.
- Score takeover risk rather than alerting on any single signal.
- Exclude corporate NAT ranges and separate programmatic auth from interactive login.
- Screen passwords against breached-credential corpora at set and at change.
- Return identical responses and timing for nonexistent and wrong-password cases.
- Deploy graduated friction keyed on population risk rather than hard blocks.
- Threat-model and instrument password reset and MFA recovery as attack surfaces.
- Treat discovery of a validated credential set as an incident even when MFA held.
The takeaway
Credential stuffing detection works when it stops measuring individual sources and starts measuring the population. Aggregate success-rate inversion catches the campaign, a long-window one-credential-many-accounts analytic catches the patient version, and post-login behavioural scoring catches what actually matters, which is takeover. Then attack the economics: screen breached passwords at the root, deploy phishing-resistant MFA, add friction that scales with risk, and harden the recovery flows the attacker moves to next. Continue with session hijacking detection, JWT misconfiguration detection, and best password managers for security teams, or browse the full Detection Engineering pillar.
Training & tools referenced
Disclosure: Some links below are affiliate links. If you buy through them, darkpwn may earn a commission at no extra cost to you. We only recommend training and tools we actually use in our own lab, and affiliate links never influence editorial coverage.
- TryHackMeAuthorized labs to practice authentication attack analysis and API defenseSecurity TrainingStart training
- 1PasswordUnique-password enforcement is the root fix for password reusePassword ManagerCompare plans
Frequently asked questions
What is credential stuffing?
Credential stuffing is the automated replay of username and password pairs stolen from one breach against a different service, betting on password reuse. It is not guessing. Every pair was valid somewhere, which is why success rates of roughly 0.1 to 2 percent still yield thousands of compromised accounts when millions of pairs are tried.
How do you detect credential stuffing?
Detect the population, not the individual request. A distributed attack keeps every source under your per-IP threshold, so per-IP rate limiting sees nothing. The reliable signals are aggregate success-rate inversion, one credential attempted across many accounts, unusually uniform client fingerprints across many IPs, and account-takeover behaviour immediately following a successful login.
Why does rate limiting not stop credential stuffing?
Because rate limiting is almost always applied per source IP, and modern credential stuffing distributes across tens of thousands of residential proxy addresses. Each address makes two or three attempts, which is indistinguishable from a forgetful user. The attack is only visible when you aggregate across sources rather than limiting within them.
What is a normal login success rate?
Most consumer platforms sit somewhere between 75 and 95 percent success on login attempts, and the exact figure matters less than its stability. Credential stuffing drives the aggregate rate sharply down because the overwhelming majority of replayed pairs fail. A sudden inversion in that ratio is one of the most reliable signals available.
Does MFA stop credential stuffing?
MFA stops the takeover but not the attack, and the distinction matters operationally. The attacker still learns which credential pairs are valid on your platform, and that validated list is itself sellable and reusable against your MFA recovery flows. Detect and block the stuffing even where MFA holds, and treat recovery paths as part of the attack surface.