DAST scanner · deep depth · auth-aware

Password Reset Scanner

Password reset is often the weakest link in authentication. For every forgot-password endpoint the scanner sends three kinds of request and reads the reply: two different emails to see whether the answer reveals which one exists, five quick repeats to see whether anything pushes back, and one request whose body it searches for the reset token itself.

DASTCriticalMediumAuth Weakness3 tests--depth deep only

Email enumeration

An endpoint counts as a reset endpoint when its URL contains /forgot-password, /forgot, /reset-password, /password-reset, /api/auth/forgot, /api/forgot-password or /api/reset-password. The first test posts two different emails one second apart and compares the replies.

Request 1 · an email that cannot exist definitely_not_a_real_user_9x8z@test.invalid Request 2 · looks like a real account admin@test.com POST POST reply 1 404 {"error":"no such account"} 27 B reply 2 200 {"message":"link sent"} 23 B reply 1 · both messages echo the address 200 {"message":"Sent to <email>"} 66 B reply 2 200 {"message":"Sent to <email>"} 36 B Decision rule status 1 ≠ status 2 OR | length 1 − length 2 | > 20 characters 404 ≠ 200: true | 27 − 23 | = 4: false MEDIUM · 0.80 · "Password reset reveals account existence" 200 ≠ 200: false | 66 − 36 | = 30: true MEDIUM · 0.80 · flagged, although the server hides nothing

1Two POSTs go to the reset endpoint with a JSON body {"email": …}: an address on the reserved .invalid domain that cannot be a user, and admin@test.com, which looks like a real one.

2This endpoint answers differently: 404 with an error for the unknown address, 200 with a confirmation for the other. The scanner records the status and the full body length of each.

3The rule is a plain OR. A different status code is enough. Body lengths are compared as raw character counts and differ only if the gap is more than 20. Here the status differs, so the finding is MEDIUM, confidence 0.80.

4A caveat that follows from counting characters: if both replies are the same safe sentence but echo the submitted address, the two bodies differ by the length of the two addresses (44 against 14, a gap of 30). That is also flagged, so a reply that repeats the email back is worth a second look when this finding appears.

Five rapid requests

The second test asks whether an attacker could flood a victim's inbox. It sends the same reset request five times in a row, with no pause, and counts how many were accepted.

Run 1 · no limit every request accepted ? 202 ? 202 ? 202 ? 202 ? 202 5/5 accepted MEDIUM · 0.75 Run 2 · a limiter exists the third request is refused 202 202 429 not sent not sent 429 seen: stop silent Run 3 · one odd reply a 400 is not a 429 202 202 400 202 202 4/5 accepted silent: needs all 5

1Five POSTs with {"email": "admin@test.com"}, sent one after another. Only a 200, 201, 202 or 204 counts as an accepted request.

2All five accepted. No 429 and no CAPTCHA step stopped the sequence: MEDIUM, confidence 0.75. An attacker could trigger thousands of reset emails or guess tokens at speed.

3If any reply is 429, the scanner returns at once with no finding: a limiter exists, and the remaining requests are never sent.

4The finding needs exactly five accepted requests. One reply that is neither 2xx nor 429 (a 400, a 500, a CAPTCHA page) makes the count four and the test stays silent, so the check fails closed toward not reporting.

Token in the reply

A reset token proves control of an inbox. It should travel by email only. The third test posts once more and searches the body of the reply with three patterns that each need a token of at least 20 characters.

Three patterns, case-insensitive a token is 20 or more of a-z 0-9 - _ . "token" : "<token>" "reset_token" : "<token>" "link" : "…token=<token>" any one match is enough CRITICAL · 0.95 the first 500 characters of the reply are saved as the evidence reply A {"message":"Check your email"} no match: silent reply B {"token":"9f3a1c77e0b24d58a6c1"} 20 chars: FLAG reply C {"token":"abc123"} 6 chars: silent reply D {"link":"…/reset?token=Kx9mPq2vL8…"} link form: FLAG

1One more POST with admin@test.com. The scanner keeps the body and tries each pattern against it. Four replies are shown here for comparison; the scanner sees one per endpoint.

2The patterns look for a JSON field named token or reset_token, or a link field whose URL carries token=. In every case the value must be 20 or more letters, digits, dashes, underscores or dots.

3A plain confirmation matches nothing. A 20-character token in the reply is CRITICAL, confidence 0.95: the attacker who requests the reset can also read the key to it.

4The length floor is the guard against noise: {"token":"abc123"} is too short to be a reset secret and is ignored. A link carrying a long token= value is caught by the third pattern.

Authenticated scans

On an authenticated deep scan the crawl's session is merged into the one HTTP client this scanner uses (it is auth-aware, #119), so a reset flow behind the login, such as an in-app change-password endpoint, is reachable. The three tests only vary the email, so the session does not change their logic.

Why it matters

Why it matters

Password reset is often the weakest link in authentication. Account takeover: if the reset token is predictable or leaked in the response, attackers can reset any account's password. Account enumeration: different replies for "email found" and "not found" let attackers build a list of valid accounts. Inbox flooding: without rate limiting, attackers trigger thousands of reset emails, causing denial of service for the user and overwhelming the email infrastructure.

Snapchat (2014)

The password reset flow allowed enumeration and confirmation of phone numbers linked to accounts, enabling targeted attacks.

Vulnerable vs fixed code

// BAD: the reply depends on the account, and carries the token
const user = await findUser(email)
if (!user) return NextResponse.json({ error: "No such account" }, { status: 404 })
const token = await createResetToken(user)
return NextResponse.json({ message: "Link sent", token })

Limits and configuration

TestRequests per endpointReports whenSeverity · confidence
Email enumeration2, one second apartstatus codes differ, or body lengths differ by more than 20 charactersMedium · 0.80
No rate limiting5, back to backall five answered 200, 201, 202 or 204; a 429 ends the testMedium · 0.75
Token in the reply1a token of 20 or more characters in one of three JSON shapesCritical · 0.95
What it deliberately stays quiet about

It runs only at --depth deep, with a 600 s timeout and a time budget checked between endpoints. Every probe is a JSON POST with an email key, so a form-encoded or field-renamed reset request is not exercised. It never sees the email itself, so it cannot tell whether a token that was sent by mail is predictable. The auth bypass scanner also checks reset replies for token-like strings (a looser substring search, HIGH), so one leaking endpoint can appear under both scanners.