Blocking Account Enumeration Attacks in SaaS Login Flows
Stop attackers from confirming which emails exist before they breach your systems.

Account enumeration is the recon step that comes before the breach you actually hear about. It's how attackers figure out which emails on your platform belong to real accounts, using nothing but your own login form's responses as a snitch. Get this wrong and every downstream defense, MFA included, is playing catch-up on a fight that's already half-lost.
This matters more for SaaS than almost any other software category. One confirmed email doesn't just unlock an inbox. In a multi-tenant world where apps are connected through shared identity credentials, it can unlock a chain of federated apps, service integrations, and admin panels that all trust the same identity. SaaS breaches jumped sharply in 2024, and once attackers get initial access, the time to compromise core systems has dropped to roughly 9 minutes. Enumeration is the step that makes that 9-minute sprint possible, because it tells the attacker exactly where to aim before they ever throw a punch. Credential stuffing made up 24.3% of all login attempts last year, according to Okta's State of Security Incident Report 2024, and stuffing only works well when the attacker already knows which accounts exist. Enumeration is what turns a shotgun into a scope.
Signals that leak account existence in a login flow: six ways attackers detect it
Login forms talk. Not in words, necessarily, but in response codes, timing, and phrasing, and attackers have gotten very good at listening.
Error message discrepancy is the classic version. "User not found" versus "Incorrect password" hands an attacker a free yes/no oracle. Script one email per request, sort the responses into two buckets, done. It's the enumeration equivalent of a vending machine that tells you which slot is empty before you even put money in.
Timing attacks are sneakier, and a lot of teams miss them. A valid email forces the server to hash the submitted password and compare it, which takes real computing time. An invalid email can just bail out early and return instantly. A documented case in Directus's password reset endpoint showed this exact flaw: supplying an invalid reset_url parameter caused a roughly 500 millisecond gap in response time between existing and non-existing users, because URL validation ran before the timing-protection logic ever fired. Translation: you can write the most carefully generic error message in the world, and the clock still rats you out.
The password reset flow is where most teams leave the back door wide open after locking the front one. "We sent you a reset email" for a real account versus a 400 error or dead silence for a fake one is basically a lookup service with a friendly UI. Rate limiting slows this down, sure, but it doesn't shut it off. An attacker running 100 requests a minute can still work through thousands of email addresses a day. OWASP's guidance here is blunt: same message, same timing, no exceptions, and route reset instructions through a side channel instead of confirming anything in the response itself.
Registration flows have the same problem in a different outfit. "This email is already registered" is a direct confirmation, and username-availability checks on signup forms carry the identical risk.
HTTP status codes leak even when the visible page looks fine. A 200 for a valid account and a 403 for an invalid one is invisible to a human eye but readable to any script bothering to check.
And then there are APIs, which get less scrutiny than login pages but carry just as much risk. Password reset APIs, profile lookups, anything that takes a user identifier and returns different output depending on whether that identifier exists, all of it is enumeration surface. APIs took 43% more attacks per host than websites in 2025, according to Indusface's State of Application Security report. OAuth and SSO flows widen this further. In documented incidents, attackers walked victims through consent screens for malicious apps disguised as legitimate tools. Once consent was granted, the attacker could make API calls as that user, enumerating connected apps and services across the victim's identity in one shot. The very thing that makes SSO convenient (one login, many apps) is also what makes a single enumerated account worth so much more.
What attackers do with a confirmed account list
A verified list of real accounts is a shopping list, and attackers spend it two ways.
First, credential stuffing: pair confirmed emails with passwords pulled from old breach dumps and run the combinations at scale. The 2025 Verizon DBIR found that 88% of attacks against basic web applications involved stolen credentials. Second, targeted phishing and business email compromise. A confirmed address is worth far more as a phishing target than a guessed one, and BEC accounted for 28% of all security incidents in early 2025. Almost all of it starts with some form of enumeration to make sure the target address is real before wasting the con.
The 23andMe breach from October 2023 is the textbook case of how small an entry point needs to be. Credential stuffing following enumeration compromised data belonging to 6.9 million users, but attackers only directly accessed around 14,000 accounts, about 0.1% of the total. Connected profile features did the rest of the work, turning a narrow foothold into a massive blast radius. PayPal's December 2022 incident hit roughly 35,000 accounts through the same stuffing playbook.
The success rate on any individual credential stuffing attempt is low, under 4%. But bots don't get tired and they don't need a high hit rate when they're running millions of attempts. Account takeover fraud reached $15.6 billion in losses in one major market. in 2024. 83% of organizations reported at least one account takeover incident that same year. And because SaaS accounts often carry OAuth tokens bridging dozens of connected apps, one compromised login isn't the end of the incident, it's the launchpad. The Snowflake campaign showed exactly this pattern, with compromised credentials cascading into breaches at downstream customers including Ticketmaster and Santander. On top of the security cost, there's now a regulatory one: Visa's VAMP mandate, effective 2026, tracks enumeration ratios directly, and merchants crossing 300,000 enumerated transactions face fines of $8 per fraudulent or disputed transaction.
Normalizing authentication responses: the highest-leverage fix
OWASP's guidance lays out the fix in one sentence: the response should return the identical HTTP status code, the identical response body, and the identical error message no matter why the login failed. It doesn't matter whether the password is wrong, the account is unknown, the account is locked, or the account is disabled. One message covers all of it: "Invalid email or password."
This can't stop at the login form, either. Password reset endpoints need the same uniform message for known and unknown addresses. Registration flows should either delay the "this email is already registered" confirmation or move it to a side channel, like emailing the existing account holder a quiet heads-up that someone tried to sign up with their address. Any API endpoint that takes a user identifier needs the same discipline applied.
The implementation detail that trips people up: test for byte-identical responses, not "close enough." Divergence sneaks in through error-handling middleware, localization strings, and framework defaults that nobody audited. In multi-tenant SaaS specifically, tenant-not-found and user-not-found conditions often get handled by separate code paths, and if those paths return different status codes, the whole normalization effort falls apart at a seam nobody was watching.
Eliminating timing side-channels in auth code paths
Normalizing the message doesn't fix the clock. A valid account triggers password hashing, which takes real time to compute. An invalid account can skip that work and return fast. The gap is measurable even when the response body is word-for-word identical, and the Directus case proves the point: URL validation fired before the timing-protection logic could kick in, so the protection existed on paper and did nothing in practice. A roughly 500 millisecond gap was enough to confirm account existence.
There are two ways to close this. One is padding the fast path: add a sleep or busy-wait to the invalid-account branch so it matches the slow path's duration. It works, but it's fragile. Speed up the hashing library later and the padding becomes too long, flipping the leak in the other direction. The sturdier approach is to just run the expensive work regardless of outcome: hash the password against a dummy value even when the account doesn't exist, and write the audit log entry either way. There's no separate fast path left to accidentally protect wrong.
The same logic applies to password reset flows. Skipping the email send for unknown addresses makes that branch faster, so send the email (or hit the mail service with a no-op) on both paths. And token hygiene matters here too: reset tokens need to be single-purpose, not recycled across reset, invite, and verification flows, with short expiry windows and revocation on any suspicious signal. A predictable token is its own separate leak sitting right next to this one.
Rate limiting, device fingerprinting, and bot detection, layered controls that slow and detect at scale
Rate limiting helps, but calling it a fix oversells it. An attacker capped at 100 requests a minute against a reset endpoint can still work through thousands of emails a day. It buys time to detect the attack. It doesn't close the door.
Apply limits across several dimensions at once, because each one alone has a hole in it:
- IP address, the simplest control, but botnets rotate through residential proxies to dodge it, and shared corporate or carrier-grade NAT means one IP might represent hundreds of legitimate users.
- Username or email, which catches distributed attacks hitting the same account from many different IPs.
- Device fingerprint, tracking browser, OS, canvas signature, and behavior patterns, so the same device gets flagged even after it rotates its IP.
- Exponential backoff, doubling the lockout window with each violation to squeeze down how fast an attacker can grind through accounts.
CAPTCHAs help less than they used to. AI-driven bots now mimic human typing rhythm and solve image challenges well enough that modern bot tooling can test thousands of accounts per second without tripping a basic rate limiter. Invisible behavioral checks (mouse movement, dwell time, background JavaScript challenges) hold up better against current bots. CAPTCHA still earns its keep on the password reset form specifically, where a generic error message can't fully substitute for user feedback, and it stops both reset flooding and enumeration at the same time.
A web application firewall adds another layer, spotting a single IP generating an unusual volume of auth requests and either blocking it outright or quietly returning a plausible-but-fake response, which poisons the attacker's data without them knowing it. Research has found that 80% of login traffic during attack spikes on large SaaS platforms was automated credential testing, so watching for volume and failure-rate anomalies catches attacks that look perfectly legitimate one request at a time.
MFA, passkeys, and the limits of credential-based authentication
MFA is the last line, not the whole wall. Even when enumeration succeeds and a stuffed password matches, MFA stops that confirmed account from becoming a compromised one, because a correct password alone still isn't enough to get in.
The honest caveat is that 65% of breached accounts already had MFA turned on, according to Obsidian Security's account takeover analysis. Adversary-in-the-middle phishing kits and session token theft can route around MFA entirely, so it's necessary and nowhere near sufficient on its own. Password reuse makes the whole problem worse before MFA even enters the picture. Research has found that 1 in 4 users reused passwords across multiple services, so a confirmed account plus a password pulled from an unrelated leak is a high-odds match before any automation even runs.
Passkeys built on FIDO2 fix this at the root instead of patching around it. There's no password to guess and no username-to-password pairing for an attacker to exploit through mismatched error responses, because the secret is a private key that never leaves the device. The enumeration oracle loses most of its value once there's nothing to guess. Passkey adoption isn't universal yet, though, so for most SaaS platforms today, the realistic stack is MFA, response normalization, constant-time responses, and layered rate limiting, all stacked together rather than any one of them carrying the load alone.
Where these vulnerabilities concentrate in modern SaaS auth architectures
Modern SaaS login isn't one form, it's five or six, each with its own leak profile: email and password, magic links, social OAuth and OIDC, enterprise SSO through SAML, and passkeys. Multi-tenancy adds a layer of its own. A user-not-found response might be perfectly normalized at the global level and still leak tenant membership if the tenant-resolution step runs on a separate code path with its own response codes.
The OAuth and SSO layer trades one risk for another. It cuts password exposure but introduces token exposure instead, and an over-permissioned OAuth token obtained through a single enumerated account can move laterally across every connected SaaS app without needing to re-authenticate anywhere. The Midnight Blizzard breach started with a password spray against a legacy test account and pivoted through OAuth tokens all the way into production mailboxes.
API-first SaaS platforms carry a quieter version of the same risk. Publicly documented or simply discoverable endpoints that accept a user identifier bypass every UI-level control, CAPTCHA and behavioral detection included, because none of that logic lives at the API layer. Non-human identities, service accounts, and API keys often go unmonitored. Obsidian Security research found 46% of organizations struggle to monitor them at all. Integration sprawl compounds this: the average tech stack runs 342 SaaS applications, climbing to 536 at large enterprises, according to Productiv research, and each integration spins up its own tokens and service accounts. 56% of organizations say they're concerned about over-privileged API access, and enumeration through a loosely guarded integration endpoint is a well-worn way around a hardened main login page.
The takeaway holds across all of it: enumeration defense has to sit at the design level of the authentication layer, applied to every endpoint that takes a user identifier, built in from the start alongside the main login form.
Building enumeration resistance into the authentication layer from the start
Authentication, user management, billing identity, and API access control tend to live in separate systems as a SaaS product grows, and every boundary between those systems is a place enumeration logic can quietly fall apart. A team can normalize the login form perfectly and still leave the billing portal's account lookup wide open, because nobody thought to check whether the same discipline applied there too.
The fix isn't a single patch, it's a standing habit: byte-identical responses, constant-time execution regardless of account status, layered rate limits across IP, identity, and device, MFA as the backstop, and a long-term path toward passkeys. Every new endpoint that touches a user identifier inherits the same rules from day one, not after a pen test flags it. Enumeration resistance is a baseline requirement of the auth layer itself. It's a property the whole auth layer either has or doesn't, and retrofitting it later always costs more than building it in from the start.


