fix(BUG-02): rate limiter now keys on real client IP + enforces default_limits
The frontend proxy fix alone was incomplete. Backend slowapi used the default
get_remote_address (request.client.host), which is the proxy's IP because
uvicorn runs without --proxy-headers — so the relayed x-forwarded-for was
ignored and all users still shared one rate-limit bucket.
- Add app/ratelimit.py: shared `limiter` + `client_ip_key` that reads
x-forwarded-for[0] → x-real-ip → peer. Replaces the three independent
Limiter(get_remote_address) instances in main.py / posts.py / prices.py
(which also had separate, non-shared storage).
- Register SlowAPIMiddleware so default_limits ("60/minute") applies to EVERY
route. Previously only the 2 decorated read endpoints were limited; all
signed-mutation routes had no rate limit at all (the "20/min per-route"
comment was aspirational — no such decorator existed).
- Add tests/test_ratelimit.py (7 tests): XFF precedence, fallbacks, two users
behind one proxy get distinct keys, middleware-registered guard.
72 tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -426,11 +426,18 @@ this for you" to "you opened it, bot manages your discipline".
|
||||
computes remaining seconds, and reschedules the task. Elapsed windows (backend
|
||||
was down longer than the time-stop period) fire immediately with `delay_seconds=0`.
|
||||
|
||||
- **Rate limit bypass via proxy x-forwarded-for deletion** (BUG-02, HIGH):
|
||||
`app/api/proxy/[...path]/route.ts` (frontend) strips `x-forwarded-for`
|
||||
before forwarding. Backend `slowapi` sees the Next.js server IP and applies
|
||||
one rate-limit bucket to ALL users. Fix: relay the real client IP from
|
||||
`req.headers.get('x-forwarded-for')` or `x-real-ip` in the proxy.
|
||||
- ~~**Rate limit bypass via proxy x-forwarded-for** (BUG-02, HIGH, FIXED 2026-05-29):~~
|
||||
Two-part fix. (1) Frontend proxy `app/api/proxy/[...path]/route.ts` now
|
||||
relays the real client IP via `x-forwarded-for` / `x-real-ip`. (2) Backend
|
||||
had the matching gap: `slowapi`'s default `get_remote_address` reads
|
||||
`request.client.host` (the proxy IP, since uvicorn runs without
|
||||
`--proxy-headers`), so the relayed header was ignored and all users still
|
||||
shared one bucket. Now a shared `app/ratelimit.py` exposes `client_ip_key`
|
||||
(reads `x-forwarded-for[0]` → `x-real-ip` → peer) used by ONE shared
|
||||
`limiter` across `main.py`, `posts.py`, `prices.py`. Also registered
|
||||
`SlowAPIMiddleware` so `default_limits` (60/min) actually applies to every
|
||||
route — previously only the 2 decorated read endpoints were limited and all
|
||||
signed-mutation routes had no limit at all. Covered by `tests/test_ratelimit.py`.
|
||||
|
||||
- ~~**`close_and_finalize` double-failure leaves DB/HL state inconsistent**
|
||||
(BUG-03, MITIGATED 2026-05-29):~~ Full two-phase-commit is out of scope.
|
||||
|
||||
Reference in New Issue
Block a user