Cloudflare Application Security study materials
Cloudflare Application Security study resources

Residential-Proxy Credential Stuffing Against a Login API

Cloudflare Application Security · Put your knowledge to work in context.

All case studies

Residential-Proxy Credential Stuffing Against a Login API

A retail bank's login API, POST /api/v1/auth/login, takes JSON {username, password} and sits behind Cloudflare with Bot Management Enterprise. Over nine days the bank sees roughly 40,000 login attempts per day across about 18,000 distinct usernames taken from an old breach dump. 1.9% of attempts succeed, and 600 accounts show successful logins from previously unseen ASNs. Traffic comes from around 900,000 residential IPs and uses real Chromium builds inside anti-detect browsers: TLS and HTTP/2 fingerprints match the claimed user agent, no challenge is being solved, and Bot Scores land between 28 and 71. Each IP makes only 3–9 attempts. The bank's own iOS app calls the same endpoint over URLSession with no cookies and no JavaScript, and must keep working. Legitimate web customers also occasionally fail logins three or four times before succeeding.

1. Why is per-request bot scoring a weak discriminator here, and which signal classes would you build the decision on instead?

Read the worked answer

Per-request scoring answers 'does this single request look automated?' Each request genuinely does not. The operator uses real Chromium in an anti-detect container, so UA, TLS/JA4, HTTP/2 settings and header order are all internally consistent. A Bot Score in the 28–71 range is precisely the inconclusive band that says the single-request evidence is weak. IP reputation is equally useless: 900,000 residential IPs at 3–9 attempts each means no individual IP is abusive.

What discriminates is the aggregate and the sequence:

  • Identity distribution. Thousands of usernames tried once or twice from unrelated IPs, with a 1.9% success rate. Real users fail repeatedly on one account; stuffing fails once per account.
  • Session context. No prior page loads, no static assets, no cookies from a browsing session - the client goes straight to POST /login.
  • Fingerprint-to-identity consistency. The same JA4/JA4H fingerprint appearing across dozens of unrelated usernames and IPs is far stronger than the fingerprint alone. Correlate it.
  • Cohort timing. Attempts per minute shaped like a scheduler - flat rate, no diurnal curve - versus human bursts.
  • Credential validity by cluster. A source cohort with a 5% success rate is not curious users.

So move the decision point from request-level to identity- and cohort-level: key on username, on fingerprint and on ASN, and score the pattern. This is as much a rules-and-aggregation problem as a model problem, and Cloudflare's detectors need the per-identity view to contribute anything.

2. Give the control set for the login endpoint, ordered by false-positive risk, including how the iOS app keeps working.

Read the worked answer
  1. Per-username throttle (lowest false-positive risk, highest value). At most 5 failed attempts per 10 minutes per username, then a managed challenge, then a 15-minute lock with a notification to the account owner. This is the single most effective control against stuffing and it never touches a user who mistypes twice.
  2. Leaked-credential detection, if the plan includes it. Check submitted username/password pairs against known-breach corpora at the edge and force a reset or block. This attacks the 1.9% success rate directly - those are real credentials from the dump, and blocking them is worth more than blocking the bots.
  3. Cohort limits. For example 200 login attempts per hour per JA4 fingerprint, challenge above that. A real browser fingerprint is shared by a handful of people, not thousands.
  4. Targeted challenge. Turnstile or a managed challenge on the web login only, triggered when the identity throttle or a cohort rule fires. Never a blanket challenge on POST /login.

iOS app: the native client cannot solve a JavaScript challenge, so do not put a path-wide challenge in front of both clients. Either split the endpoint (/api/v1/auth/login for web, /api/v2/auth/login for app) or validate an app-bound credential at the edge - mTLS, or a signed header whose key is provisioned with the app - and exempt only requests presenting it. Never exempt on user agent: MyBank/5.2 iOS is a string anyone can send.

Real users: throttle failures only, so a customer who succeeds on the sixth attempt is unaffected. That is what keeps the 3–4-failure population out of the lockout, and it is the main reason to prefer the per-username control over IP-based ones.

3. What do you do about the 600 accounts, and what does the arithmetic imply about your detection?

Read the worked answer

They are compromised, not suspicious - the logins succeeded.

  1. Contain. Invalidate all active sessions for the affected accounts and force a password reset at next login. Block the source cohorts - fingerprints and ASN clusters, not the individual IPs, which are disposable - so re-entry with a new credential fails too.
  2. Check for the attacker's own reset. If the actor changed the recovery email or phone before you acted, your reset hands the account back to them. Look for profile changes, new payees or beneficiaries, password and MFA changes in the window before you notify.
  3. Notify with specifics, with a deadline for the reset. Regulatory obligations apply if this is a bank and funds moved.
  4. Fix the credential, not just the session. The passwords are public. Prompt MFA enrolment for these accounts and for any account whose username appears in the dump.
  5. Reconcile the numbers. If 1.9% of 40,000 attempts per day succeeded across nine days, the success count is closer to 760 per day than 600 total. Before closing the incident, explain the discrepancy - sampling, deduplication of the same account, or a detection gap.
  6. Pre-empt the next campaign. Compute breach-list overlap for the whole customer base and force resets proactively. That converts the next run of this attack into a non-event, which is the only durable outcome.