Auth Bypass Scanner
Authentication is the front door. This scanner knocks on it without a session and reads what the server gives away: which accounts exist, whether guessing is ever slowed down, whether admin/admin works, whether a reset token comes back in the HTTP reply, and whether a protected endpoint opens for a missing or fake Authorization header.
Username enumeration
The scanner first picks endpoints by URL: login endpoints contain /login, /signin, /sign-in, /auth/login, /api/auth, /api/login or /api/signin. Each one gets two logins that differ only in the email. Three independent comparisons then decide whether the server told the two apart.
1The scanner sends two logins with no session. One uses nonexistent_user_test_00@test.invalid, which cannot be a real account, the other admin@test.com. Both carry the same wrong password, so any difference in the reply comes from the email alone.
2The server answers both with 401, but the bodies and the timings differ. The scanner keeps the first 500 characters of each body, the status and the elapsed time.
3Message check. Identical bodies end it. Otherwise one body must hold a phrase from a known pair and the other body the matching partner: "user not found" with "invalid password", "no account" with "incorrect password", "does not exist" with "invalid credentials", and five more. Two bodies that merely differ (a request id, a timestamp) do not count. HIGH, confidence 0.85.
4Timing check. A gap above 0.5 s between the two replies suggests the server only hashes a password when the account exists. It is one sample each, so confidence is lower: MEDIUM, 0.65.
5Status check. Two different status codes would be MEDIUM, 0.70. Here both are 401, so this check stays quiet. One endpoint can raise zero, one, two or all three findings.
Lockout and default credentials
These two phases hit the same login endpoints. One repeats a wrong login to see whether the server ever pushes back; the other tries seven well-known credential pairs.
1Lockout. The scanner sends the same wrong login ten times, 0.2 s apart. A server that locks accounts would start answering 423 (Locked).
2It watches every status. A 423 or 429 (or a lock message such as "too many attempts") means a lock or throttle works and the scanner reports nothing. Otherwise an 11th attempt is sent, and if it is not throttled either the finding is MEDIUM, confidence 0.60. The rate limit scanner measures the throttle itself.
3Default credentials. Seven pairs run in a fixed order, half a second apart. A wrong-password control login must be rejected first. A pair succeeds only when the reply is 2xx, is JSON rather than an HTML page, and carries a non-empty credential field such as access_token. A 200 that just says {"error":"bad login"} is not a success.
4The third pair, admin@admin.com / admin123, returns a token. That is CRITICAL, confidence 0.95, with the request and reply saved as evidence. The scanner stops at the first hit, so the last four pairs are never sent.
Token leak and header bypass
Two more phases look at endpoints other than the login form: password-reset endpoints (/forgot-password, /reset-password, /password-reset and four more spellings), and endpoints that ought to require a login.
1Reset endpoints get one POST for an email that cannot exist. A well-built reset form answers every email the same way and sends the link by mail. The page shows a safe reply and a leaking one side by side.
2Leak check. The scanner lower-cases the first 500 characters of the body, then the response headers, and looks for seven token-like strings. A hit is HIGH, confidence 0.90, because anyone who can submit the form can then reset that account. The check is a plain substring search, so a header such as Set-Cookie: csrftoken=… also contains token=.
3Header bypass. Endpoints with requires_auth set, or a path containing /api/user, /api/account, /api/profile, /api/settings, /api/admin, /dashboard/api or /api/me, are tried with three headers, in order. The first two are refused.
4The third, a Basic header for admin:admin, is answered 200. A control request with an invalid bearer token is sent first. A 2xx that is not an HTML page or a failure body is HIGH, confidence 0.85 when that control was denied (401/403), and a Medium "serves data without authentication" note (0.60) when the invalid token was served too. One bypass per endpoint is enough evidence. The endpoints are ranked by the shared prioritizer so the most auth-looking ones go first.
Why it carries no session
Most deep scanners get the crawl's login session injected into every request. This one is the exception, on purpose (issue #119). Every phase depends on the absence or forging of authentication.
1An authenticated deep scan captures a session during the crawl and hands it to every HTTP scanner that carries the auth mixin. That is right for injection or CSRF probes, which must get past the login wall.
2Here it would break the test. The "no Authorization header" probe sends an empty header set, but the client's base headers still apply, so the session would be sent anyway and the endpoint would answer 200. The probe would report a bypass that does not exist. The empty-Bearer and Basic probes overwrite Authorization but a session cookie would still ride along.
3As built, the client carries only a User-Agent. The anonymous probe gets its honest 401. Username enumeration, lockout and default-credential probes need the same clean slate.
4The scanner simply does not inherit the auth mixin. The orchestrator hands a session only to scanners that expose _auth_headers, so this one is skipped.
Why it matters
Authentication is the front door. If attackers can enumerate usernames, there is no lockout and default credentials work, then guessing becomes cheap. Targeted attacks: knowing which emails have accounts lets attackers focus phishing and credential stuffing. Brute force: without lockout, tools try thousands of passwords per minute. Instant access: admin/admin gives admin access with zero effort.
No account lockout on the Find My iPhone API allowed brute-force password attacks against celebrity accounts.
Vulnerable vs fixed code
// BAD: a different message for each cause, and no limit on tries
if (!user) {
return NextResponse.json({ error: "User not found" }, { status: 404 })
}
if (!validPassword) {
return NextResponse.json({ error: "Invalid password" }, { status: 401 })
}// GOOD: Same error message for all auth failures
if (!user || !validPassword) {
return NextResponse.json(
{ error: "Invalid email or password" }, // Same message regardless of cause
{ status: 401 }
)
}
// GOOD: Account lockout after N attempts
const attempts = await getLoginAttempts(email)
if (attempts >= 5) {
return NextResponse.json({ error: "Account locked. Try again in 15 minutes." }, { status: 423 })
}The same message and status are not enough if one path hashes a password and the other returns early. Run the same work, or a dummy hash, when the account does not exist, so the 0.5 s timing check has nothing to see. The lockout check recognises 423, 429 and common lock messages, so a lock that answers with another code and no message is still reported even though it works.
Limits and configuration
| Phase | Requests | Reports when | Severity · confidence |
|---|---|---|---|
| Username enumeration | 2 per login endpoint | a known phrase pair in the two bodies; timing gap above 0.5 s; different status codes | High · 0.85 / Medium · 0.65 / Medium · 0.70 |
| Reset token leak | 1 per reset endpoint | one of 7 token strings in the first 500 body characters or in the headers | High · 0.90 |
| Account lockout | 10, plus 1 if needed | the last status is not 423 | Medium · 0.60 |
| Default credentials | up to 7 per login endpoint, plus 1 control | control rejected; 2xx JSON with a non-empty token field | Critical · 0.95 |
| Auth header bypass | up to 3 per protected endpoint, plus 1 control | non-HTML 2xx to the no-header, empty-Bearer or Basic admin:admin request while an invalid token is denied | High · 0.85 |
It only runs at --depth deep, with a 1800 s timeout and a shared time budget across the five phases: when the budget runs out the current phase stops and each later phase stops on its first endpoint, returning what was found. The probes always send a JSON body with email and password keys, so a login that expects other field names or form encoding is not exercised the way a real client would. Each probe sleeps 0.5 s, and the whole run is logged as an authorized test. Only run it against systems you are permitted to test.