Web Auth Defenses Testing
Purpose
Test login/account-defense controls the way a bug-bounty reviewer scores them: determine scope
of any lockout, find logic anomalies, and confirm (or refute) hosted-app auth weaknesses —
without locking real users or sustaining an outage.
When to use
- Target exposes
POST /api/auth/{login,register,forgot-password,mfa/verify} or similar.
- You see a "too many attempts" / lockout / rate-limit response during auth testing.
- You're assessing a Next.js/Vercel or other hosted-SPA app with separate auth + backend services.
- Companion to
web-pentest (this skill drills the auth-defense sub-area in depth).
Phase 1 — Lockout scope determination (MOST IMPORTANT, drives severity)
A failed-login lockout is keyed one of three ways; impact differs by ~10x:
- per-account → only the targeted victim denied (Medium/High)
- per-IP → only attacker's source throttled (Low)
- GLOBAL/shared→ entire site cannot log in (Critical, CVSS ~9.1, full auth outage)
Probe recipe (run in order, NEVER use real users' emails)
# (a) ~6 bad logins to a RANDOM fresh email:
for i in $(seq 1 6); do
curl -s -X POST https://TARGET/api/auth/login -H "Content-Type: application/json" \
-d "{\"email\":\"locktest_$i@randomexample.com\",\"password\":\"wrongpass\"}"
done
# (b) test a DIFFERENT fresh email once:
curl -s -X POST https://TARGET/api/auth/login -H "Content-Type: application/json" \
-d '{"email":"other_fresh@randomexample.com","password":"wrongpass"}'
# "N attempts remaining" -> per-EMAIL (per-account)
# "locked" + SAME resetTime -> GLOBAL
# (c) DEFINITIVE: brand-new email never contacted before:
curl -s -X POST https://TARGET/api/auth/login -H "Content-Type: application/json" \
-d "{\"email\":\"neverseen_$(date +%s)@fresh.io\",\"password\":\"x\"}"
# locked w/ identical resetTime as (a) -> GLOBAL shared counter CONFIRMED.
Capture resetTime each time; identical timestamps across unrelated emails prove a shared
counter. Compare to date -u for lock-window duration. Always run (c) before scoring.
Phase 2 — Operational safety (do not cause harm)
- Use only random never-seen emails as lock targets. Do NOT probe guessed real addresses
(admin@, support@, info@) — you may lock real users and trigger a real global outage.
- Stop sending login requests once the lockout is confirmed. A global lock may not clear
within a short window; continued probing sustains the outage. Confirm, then cease.
- If a real-looking address was accidentally locked, note it as impact evidence, then stop.
- Registering a test account to reach authenticated areas may fail while a global lock is active
— wait it out or use a fresh source rather than hammering.
Phase 3 — Error-handling & enumeration differentials
- Invalid email format vs valid format vs valid + wrong password often yield different
messages (
400 "Invalid email" vs 401 "Invalid credentials. N remaining"). Low (enum aid).
/forgot-password returning 500 for every valid-format input (while 400 for bad format)
= broken reset flow / weak error handling (Medium). Record the exact body.
Phase 4 — Auth-route method / logic anomalies
Phase 5 — Hosted-app (Next.js/Vercel) auth probing
httpx -tech-detect → Vercel, HSTS, sometimes C3.js; apex IP is Vercel anycast.
- Protected routes: expect
307 → /login (GET) and /api/dashboard/* → 401 without session.
That is access control WORKING — record as confirmed non-issue, not a finding.
- Source maps
<chunk>.js.map → usually 403 (safe). Hidden .env/.git/.svn → 403/404.
- CORS
Access-Control-Allow-Origin: * is LOW unless Access-Control-Allow-Credentials: true
also present.
- Missing
X-Frame-Options / CSP frame-ancestors / X-Content-Type-Options / Referrer-Policy
→ clickjacking Low finding.
- Mass-assignment: send
role":"admin" / isAdmin":true in register; verify server echoes
role":"user" (rejected) vs honors it.
Phase 6 — Separate backend gateway discovery
/docs often embeds a real backend URL in sample curl (e.g. https://<svc>.up.railway.app).
Probe separately: $B/health (leaks service name/mode/uptime), $B/v1/chat/completions (401
with distinct no-key vs bad-key message). Flag CORS * only if a valid key can reach a browser
context; if frontend proxies server-side, it's low-risk. Info/Low.
Phase 7 — Platform variants: Moodle (and classic PHP form-logins)
Moodle logins are form-POST with a MoodleSession cookie, not JSON APIs — adapt Phase 1:
- CSRF token (
logintoken) may not block scripts: POSTing /login/index.php with an EMPTY logintoken field was processed normally in the wild (303 → form re-render "Invalid login"). Test once with an empty value; don't assume the token defeats scripted probes.
- Rate-limit probe shape: 8 failed POSTs, fake username (
rateprobe_nonexistent), 0.3 s interval → alternating 303/200 with "Invalid login" each time and zero throttle/lockout/captcha keywords = NO rate-limit observed up to N attempts. Report as a bounded lower bound (Medium when accounts are students/staff: credential stuffing path); never claim "unlimited". Stop at the first throttle signal or ~8 attempts, whichever first.
- Parallel surface:
/login/token.php (mobile web service, JSON) processes bad credentials too — run the same gap check; it's what mobile-app stuffing actually hits.
- forgot-password is NOT an enum oracle here: valid and invalid usernames both return 200 with generic text; only sesskey/random-token bytes differ between bodies. Hash-diff full bodies and inspect diff fragments before claiming enumeration — status-code equality proves nothing (opposite of the JSON-API differential in Phase 3).
Reporting
- Score lockout by scope: GLOBAL → Critical; per-account → High/Medium; per-IP → Low.
- Always include the resetTime comparison as evidence for a global finding.
- Provide a self-contained
poc.py that reproduces with a random email and stops after confirming.
References
references/lockout_and_hosted_app_testing.md — full probe recipes, curl snippets, and
tooling notes for lockout-scope determination, hosted-app specifics, and backend discovery.
1---2name: web-auth-defenses-testing3description: Methodology for testing authentication defenses on web apps — login lockout/rate-limit scope determination (global vs per-account vs per-IP), forgot-password error handling, auth-route method/logic anomalies, and hosted-app (Next.js/Vercel) auth probing. Produces correctly-scored findings (Critical vs Medium) and safe, non-destructive testing.4---56# Web Auth Defenses Testing78## Purpose9Test login/account-defense controls the way a bug-bounty reviewer scores them: determine *scope*10of any lockout, find logic anomalies, and confirm (or refute) hosted-app auth weaknesses —11without locking real users or sustaining an outage.1213## When to use14- Target exposes `POST /api/auth/{login,register,forgot-password,mfa/verify}` or similar.15- You see a "too many attempts" / lockout / rate-limit response during auth testing.16- You're assessing a Next.js/Vercel or other hosted-SPA app with separate auth + backend services.17- Companion to `web-pentest` (this skill drills the auth-defense sub-area in depth).1819## Phase 1 — Lockout scope determination (MOST IMPORTANT, drives severity)2021A failed-login lockout is keyed one of three ways; impact differs by ~10x:22- **per-account** → only the targeted victim denied (Medium/High)23- **per-IP** → only attacker's source throttled (Low)24- **GLOBAL/shared**→ entire site cannot log in (Critical, CVSS ~9.1, full auth outage)2526### Probe recipe (run in order, NEVER use real users' emails)27```bash28# (a) ~6 bad logins to a RANDOM fresh email:29for i in $(seq 1 6); do30 curl -s -X POST https://TARGET/api/auth/login -H "Content-Type: application/json" \31 -d "{\"email\":\"locktest_$i@randomexample.com\",\"password\":\"wrongpass\"}"32done3334# (b) test a DIFFERENT fresh email once:35curl -s -X POST https://TARGET/api/auth/login -H "Content-Type: application/json" \36 -d '{"email":"other_fresh@randomexample.com","password":"wrongpass"}'37# "N attempts remaining" -> per-EMAIL (per-account)38# "locked" + SAME resetTime -> GLOBAL3940# (c) DEFINITIVE: brand-new email never contacted before:41curl -s -X POST https://TARGET/api/auth/login -H "Content-Type: application/json" \42 -d "{\"email\":\"neverseen_$(date +%s)@fresh.io\",\"password\":\"x\"}"43# locked w/ identical resetTime as (a) -> GLOBAL shared counter CONFIRMED.44```45Capture `resetTime` each time; identical timestamps across unrelated emails prove a shared46counter. Compare to `date -u` for lock-window duration. **Always run (c) before scoring.**4748## Phase 2 — Operational safety (do not cause harm)49- Use only random never-seen emails as lock targets. Do NOT probe guessed real addresses50 (admin@, support@, info@) — you may lock real users and trigger a real global outage.51- **Stop sending login requests once the lockout is confirmed.** A global lock may not clear52 within a short window; continued probing *sustains* the outage. Confirm, then cease.53- If a real-looking address was accidentally locked, note it as impact evidence, then stop.54- Registering a test account to reach authenticated areas may fail while a global lock is active55 — wait it out or use a fresh source rather than hammering.5657## Phase 3 — Error-handling & enumeration differentials58- Invalid email *format* vs valid *format* vs valid + wrong password often yield different59 messages (`400 "Invalid email"` vs `401 "Invalid credentials. N remaining"`). Low (enum aid).60- `/forgot-password` returning `500` for every valid-format input (while `400` for bad format)61 = broken reset flow / weak error handling (Medium). Record the exact body.6263## Phase 4 — Auth-route method / logic anomalies64- After `OPTIONS` on an auth route, read `Allow:` (e.g. `DELETE, OPTIONS, POST`). Test surprises:65 ```bash66 curl -s -X DELETE https://TARGET/api/auth/login -H "Content-Type: application/json" -d '{}'67 # 200 {"success":true} without auth = logic anomaly (Medium)68 ```69- `PUT`/`TRACE` should be `405`. Method-override headers should be ignored.7071## Phase 5 — Hosted-app (Next.js/Vercel) auth probing72- `httpx -tech-detect` → `Vercel`, `HSTS`, sometimes `C3.js`; apex IP is Vercel anycast.73- Protected routes: expect `307 → /login` (GET) and `/api/dashboard/*` → `401` without session.74 That is access control WORKING — record as confirmed non-issue, not a finding.75- Source maps `<chunk>.js.map` → usually `403` (safe). Hidden `.env`/`.git`/`.svn` → `403/404`.76- CORS `Access-Control-Allow-Origin: *` is LOW unless `Access-Control-Allow-Credentials: true`77 also present.78- Missing `X-Frame-Options` / `CSP frame-ancestors` / `X-Content-Type-Options` / `Referrer-Policy`79 → clickjacking Low finding.80- Mass-assignment: send `role":"admin"` / `isAdmin":true` in register; verify server echoes81 `role":"user"` (rejected) vs honors it.8283## Phase 6 — Separate backend gateway discovery84- `/docs` often embeds a real backend URL in sample `curl` (e.g. `https://<svc>.up.railway.app`).85 Probe separately: `$B/health` (leaks service name/mode/uptime), `$B/v1/chat/completions` (40186 with distinct no-key vs bad-key message). Flag CORS `*` only if a valid key can reach a browser87 context; if frontend proxies server-side, it's low-risk. Info/Low.8889## Phase 7 — Platform variants: Moodle (and classic PHP form-logins)9091Moodle logins are form-POST with a `MoodleSession` cookie, not JSON APIs — adapt Phase 1:9293- **CSRF token (`logintoken`) may not block scripts**: POSTing `/login/index.php` with an EMPTY `logintoken` field was processed normally in the wild (303 → form re-render "Invalid login"). Test once with an empty value; don't assume the token defeats scripted probes.94- **Rate-limit probe shape**: 8 failed POSTs, fake username (`rateprobe_nonexistent`), 0.3 s interval → alternating 303/200 with "Invalid login" each time and zero throttle/lockout/captcha keywords = NO rate-limit observed up to N attempts. Report as a bounded lower bound (Medium when accounts are students/staff: credential stuffing path); never claim "unlimited". Stop at the first throttle signal or ~8 attempts, whichever first.95- **Parallel surface**: `/login/token.php` (mobile web service, JSON) processes bad credentials too — run the same gap check; it's what mobile-app stuffing actually hits.96- **forgot-password is NOT an enum oracle here**: valid and invalid usernames both return 200 with generic text; only sesskey/random-token bytes differ between bodies. Hash-diff full bodies and inspect diff fragments before claiming enumeration — status-code equality proves nothing (opposite of the JSON-API differential in Phase 3).9798## Reporting99- Score lockout by scope: GLOBAL → Critical; per-account → High/Medium; per-IP → Low.100- Always include the resetTime comparison as evidence for a global finding.101- Provide a self-contained `poc.py` that reproduces with a random email and stops after confirming.102103## References104- `references/lockout_and_hosted_app_testing.md` — full probe recipes, curl snippets, and105 tooling notes for lockout-scope determination, hosted-app specifics, and backend discovery.