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.
src/routes/auth.rs::loginhas 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 notower_governor-style layer applied anywhere inmain.rs. Add IP-based (and/or account-based) rate limiting middleware on/api/auth/loginand/api/auth/registerto mitigate brute-force and mass-registration abuse.Additional Notes
Marked
spikealongside the fixed-scope work because "what rate-limiting strategy fits this deployment model" is a genuine design question —src/main.rsbuilds oneRouterbehindaxum::servewith 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-memorytower_governor-style layer is sufficient and far simpler than adding Redis).Investigation questions
main.rs::run_keeper_loop— thoughFOR UPDATE SKIP LOCKEDinSubscriptionService::run_due_executionssuggests 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 alogin_attemptstable with a unique constraint window) rather than in-memory state.src/commits/fixmiddleware_rate_limiter_key_b.rsfilename (in the non-functional decoysrc/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)
tower::Layer(e.g.tower_governoror a hand-rolledArc<Mutex<HashMap<key, TokenBucket>>>, mirroring the existing cache pattern already used insrc/services/rate.rs::RateService) applied specifically to/api/auth/loginand/api/auth/registerinsrc/routes/mod.rs, keyed on a composite of client IP (fromX-Forwarded-For/SetRequestIdLayer-adjacent connect info — already usingtower_http's request-id middleware, so the infra for reading connection info exists) and the submitted email/lowercased.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.429 Too Many Requestswith aRetry-Afterheader once the threshold is hit, and log the lockout event (structured logging already exists viatracing, seemain.rs'stracing_subscribersetup) for later abuse-pattern review.Testing strategy
N+1th is rejected with429before it even reachesdecode_jwt/password comparison.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.