No rate limiting on a login API: 30 failed attempts, zero 429s, 0.157 s mean latency
Target redacted. This finding is not submitted to the program nor publicly disclosed yet, so hostnames are replaced with redacted.example and account identifiers are removed. The technical detail is kept intact.
Summary
POST /api/login applied no throttling: 30 consecutive failures produced no 429, no lockout, no CAPTCHA and no measurable shadow throttling (latency stayed flat). Combined with the email enumeration on the same host, an attacker can build a valid account list and then brute force it unattended.
Evidence
- n = 30 failed logins, sequentially and concurrently.
- 100% of responses
422with the same shape. - No
429, noRetry-After, no account lock. - Mean latency 0.157 s (no slowdown as attempts accumulated).
- The same host also exposed the reset endpoint that discloses account existence.
Impact
Credential stuffing against a staff accreditation system: a single valid account is a foothold into personnel data and event operations. Severity is Medium on its own and High when chained with the enumeration finding.
Reproduction
POST /api/login HTTP/1.1
Host: redacted.example
Content-Type: application/json
X-XSRF-TOKEN: <token>
Origin: https://app.redacted.example
{"email":"known@redacted.example","password":"wrong-guess-<n>"}
Repeat 30 times: every attempt returns 422, never 429.
Remediation
- Per-account and per-IP throttling with exponential backoff.
- Progressive CAPTCHA after N failures.
- Alerting on distributed low-and-slow patterns, not just per-IP spikes.
Lesson
"Rate limiting" needs to be measured, not assumed. Thirty requests and a latency table is evidence; "it feels brute-forceable" is not.