AP-0403Automation & fraudSeverity · Medium

A reset flow that reveals which accounts exist

A password reset form answers differently depending on whether the address has an account. The difference lets an attacker sort a list of millions into real customers, then aim the rest of the attack only at them.

At a glance

Controls
OWASP API2:2023 · SOC 2 CC6.1 · ISO 27001 A.9.4
LikelihoodCommon across signup, login and reset
Effort to exploitLow: compare responses at scale
Blast radiusYour entire customer list, confirmed
What review catchesLittle: each response reads fine

Knowing who your customers are is valuable to an attacker before they touch a single password. A reset form that says one thing for a real address and another for an unknown one hands over that list. The signal can be the wording, the status code, a redirect or the time the response takes, and any one of them turns a guess into a fact.

Why it happens

Helpful error messages are the usual cause. Telling a user that no account exists for an address feels like good service, and telling them a reset email is on its way feels honest. Each message is reasonable alone, and together they let anyone tell the two cases apart. Timing leaks the same fact without any message, when the real path sends an email and the unknown path returns at once.

How the attack unfolds

An attacker submits a large list of addresses and records how the product responds to each. The addresses that produce the account-exists signal are now confirmed customers. That confirmed list is the input to everything that follows: credential-stuffing aimed only at real accounts, phishing that names a service the target actually uses, and support-desk social engineering that opens from a confirmed fact.

The reset flow never leaked a password. It leaked the list of who has one.MATT research team

Why review misses it

Each response is correct on its own, so a reviewer reading one at a time sees nothing wrong. The leak is in the difference between two responses, which only appears when you send both and compare, and timing differences do not appear in the code at all.

Where it hides

What a reviewer can look for in the code and in the behaviour, before anyone proves it.

  • Reset, signup or login responses differ in wording, status or redirect for known and unknown accounts.
  • The response time differs because one path sends an email and the other returns immediately.
  • There is no rate limit on how many addresses one source can probe.
  • Signup rejects a duplicate address with a message that confirms the address is taken.

A real instance

A reset form returned a reset-sent message in well under a tenth of a second for unknown addresses and close to a second for real ones, because real ones waited on the email. A list of email addresses was sorted into customers and non-customers in an afternoon, purely on timing, without a word of difference in the responses.

Details altered to protect the customer. Pattern, timing and outcome are as found.

How MATT tests it

The proof runs against the deployed product, on every release, whoever wrote the code.

  1. 01Submit known and unknown addresses to reset, signup and login, and compare wording, status, redirects and timing.
  2. 02Probe at volume from one source to test whether a rate limit exists and holds.
  3. 03Confirm the enumerated list against a second flow to rule out false positives.
  4. 04Check that the same neutral response is returned whether or not the account exists.

The evidence

Before the fix
  1. POST /reset (real address)200 · ~40ms“Check your inbox for a reset link”
  2. POST /reset (unknown address)200 · ~900ms“No account found for that email”
After the fix
  1. POST /reset (real address)200 · ~900ms“If an account exists, a link is on its way”
  2. POST /reset (unknown address)200 · ~900ms“If an account exists, a link is on its way”
The rule, in plain English

A recovery or signup flow must not reveal whether an account exists.

Executable check

Reset, signup and login must return an identical response and comparable timing for known and unknown accounts, on every release.

Every rule MATT holds →

What the fix looks like

  1. 01

    Answer identically

    Return the same neutral message, status and redirect whether or not the account exists.

  2. 02

    Match the timing

    Make both paths take the same time, so the delay of sending an email cannot be measured.

  3. 03

    Rate-limit the probe

    Throttle and monitor reset and signup attempts per source, so enumeration at scale is slow and visible.

A check that was never written is found by proving the deployed behaviour, fixed with the engineer who shipped it, and kept fixed by running the control on every change. What is ExploitOps? →

ExploitOps discovery call

Understand where your security loop breaks.

A 15-minute call with a MATT co-founder to understand your product and whether an ExploitOps Review is worth doing. If there is a real need, we scope one with the research team.