Skip to content

No rate limiting on /api/auth/login enables credential-stuffing / brute force #18

Description

@abayomicornelius

src/routes/auth.rs::login has no per-IP or per-account rate limiting or lockout mechanism — an attacker can attempt unlimited password guesses against any email address.

The Cargo.toml/README mention "rate limiting" only in the context of Stellar rate quotes, not auth throttling, and there's no tower_governor-style layer applied anywhere in main.rs. Add IP-based (and/or account-based) rate limiting middleware on /api/auth/login and /api/auth/register to mitigate brute-force and mass-registration abuse.

Additional Notes

Marked spike alongside the fixed-scope work because "what rate-limiting strategy fits this deployment model" is a genuine design question — src/main.rs builds one Router behind axum::serve with no indication of whether this ever runs as multiple replicas behind a load balancer (in which case an in-process limiter is useless — each replica would allow the full quota independently) or as a single instance (in which case an in-memory tower_governor-style layer is sufficient and far simpler than adding Redis).

Investigation questions

  • Given the keeper design already assumes a single logical instance can safely run the background loop (main.rs::run_keeper_loop — though FOR UPDATE SKIP LOCKED in SubscriptionService::run_due_executions suggests the authors did anticipate multiple instances for the keeper specifically), is horizontal scaling of the HTTP layer itself in scope? If yes, rate limiting needs a shared store (Redis, or the same Postgres via a login_attempts table with a unique constraint window) rather than in-memory state.
  • Should the limit be per-IP, per-account (email), or both? Per-IP alone is defeated by any residential proxy/botnet; per-account alone allows an attacker to hammer one arbitrary victim's account from unlimited IPs. The historically-referenced src/commits/fixmiddleware_rate_limiter_key_b.rs filename (in the non-functional decoy src/commits/ directory) hints a real implementation once keyed on something that turned out to be wrong (possibly IP alone, or a header spoofable by the client) — worth deciding the correct key up front rather than repeating that mistake.

Implementation sketch (once scope is settled)

  • Minimal single-instance fix: a tower::Layer (e.g. tower_governor or a hand-rolled Arc<Mutex<HashMap<key, TokenBucket>>>, mirroring the existing cache pattern already used in src/services/rate.rs::RateService) applied specifically to /api/auth/login and /api/auth/register in src/routes/mod.rs, keyed on a composite of client IP (from X-Forwarded-For/SetRequestIdLayer-adjacent connect info — already using tower_http's request-id middleware, so the infra for reading connection info exists) and the submitted email/lowercased.
  • Stronger: persist failed-attempt counts in Postgres (a new login_attempts(email, ip, attempted_at) table) so limiting survives process restarts and works correctly if the API is ever horizontally scaled — ties directly into the first investigation question above.
  • Either way, return 429 Too Many Requests with a Retry-After header once the threshold is hit, and log the lockout event (structured logging already exists via tracing, see main.rs's tracing_subscriber setup) for later abuse-pattern review.

Testing strategy

  • Simulate N+1 failed login attempts from the same IP/account within the window and assert the N+1th is rejected with 429 before it even reaches decode_jwt/password comparison.
  • Assert the limiter resets after the configured window elapses (time-mocked test, not a real sleep).
  • If persisted to Postgres, a test asserting two concurrent "instances" (two SubscriptionService-style service structs sharing one pool) enforce the same combined limit — proving the design actually solves the multi-replica case if that's the direction chosen.

Metadata

Metadata

Assignees

No one assigned

    Labels

    backendBackend service logicsecuritySecurity concernvery hardVery difficult / senior-level bounty issue

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions