At a glance
- Controls
- OWASP API2:2023 · SOC 2 CC6.1 · ISO 27001 A.9.4
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
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.
- 01Submit known and unknown addresses to reset, signup and login, and compare wording, status, redirects and timing.
- 02Probe at volume from one source to test whether a rate limit exists and holds.
- 03Confirm the enumerated list against a second flow to rule out false positives.
- 04Check that the same neutral response is returned whether or not the account exists.
The evidence
- POST /reset (real address)200 · ~40ms“Check your inbox for a reset link”
- POST /reset (unknown address)200 · ~900ms“No account found for that email”
- POST /reset (real address)200 · ~900ms“If an account exists, a link is on its way”
- POST /reset (unknown address)200 · ~900ms“If an account exists, a link is on its way”
A recovery or signup flow must not reveal whether an account exists.
Executable checkReset, 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
- 01
Answer identically
Return the same neutral message, status and redirect whether or not the account exists.
- 02
Match the timing
Make both paths take the same time, so the delay of sending an email cannot be measured.
- 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? →