Skip to content

SEC: /api/token rate limiter is a platform-wide DoS primitive — 4 bad attempts locks out all users for 10 minutes (AISEC-H2) #591

Description

@vybe

Summary

The admin login endpoint POST /api/token enforces a global per-IP bucket: 4 failed attempts triggers a 599-second lockout. Because the bucket is per-IP and all users behind a corporate NAT, VPN, or CDN share the same egress IP, any user who fat-fingers their password 4 times locks out every other user at that IP. The 599-second lockout duration also doubles as a Denial-of-Service primitive — an attacker with any rotating proxy can keep a target organisation locked out indefinitely.

Context

AISEC (scan 3aad5469, 2026-04-28), CVSS 7.5 (AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H), CWE-307.

Two concrete problems:

  1. Credential attack — per-IP buckets are bypassable trivially via rotating proxy / Tor / ipv6 /64, so the protection is cosmetic against motivated attackers.
  2. DoS — once tripped, the 599s global lockout blocks all users sharing that source IP; an attacker can cycle proxies to hold the lockout continuously.

Note: UnderDefense issue #176 (Email OTP rate limiting) was fixed separately. The /api/token endpoint has a different and broken rate-limit design that was not addressed by that fix.

Acceptance Criteria

  • Rate limit split into two independent buckets:
    • Per-account (sliding window, e.g. 5 fails / 15 min / account) — never affects other accounts
    • Per-IP (e.g. 30 fails / 5 min / IP) — high enough to not impact legitimate shared-IP users
  • Replace global lockout with progressive delay (e.g. 1s → 2s → 4s → … per-IP) rather than a hard block
  • CAPTCHA-after-3-fails preferred over hard lockout (lockout makes endpoint unusable to legitimate users; CAPTCHA does not)
  • Security log event emitted on lockout state-change so operators see when the global bucket is being exercised
  • Test: assert that 4 rapid bad-password attempts from IP-A do not affect a valid login attempt from IP-B

Technical Notes

  • Rate limit logic lives in routers/auth.py — look for check_login_rate_limit
  • The 599-second lockout duration appears to be a redis TTL — consider replacing with a per-IP exponential backoff TTL that resets on success
  • Email OTP endpoint (/api/auth/email/verify) was separately fixed in SEC: Email OTP Verification Lacks Rate Limiting #176; validate that fix used per-account counters and did not introduce the same global-IP pattern

Metadata

Metadata

Assignees

Labels

complexity-lowComplexity: low (board points 1-3)pentestFrom penetration testing reportpriority-p1Critical pathsecuritySecurity vulnerabilityseverity-highHigh severity security findingtheme-securityTheme: Security

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions