From 8d17a5cc161c78b00d0226b95c31c177caa3826e Mon Sep 17 00:00:00 2001 From: Edward Oliveira Date: Thu, 7 May 2026 14:55:40 -0300 Subject: [PATCH] Skip IP rate counter for anonymous /api/ requests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Anonymous API requests now pass through after the quota check without incrementing rl:ip:{ip}:reqs or writing a block key. A misbehaving script or JS snippet behind a NAT IP can no longer lock out the org's page requests by hammering /api/. Enforcement for anonymous /api/: - nginx sapl_api zone (60r/m, burst=120) — burst gate - API quota (500/day, 3500/week) — daily cap Authenticated /api/ still falls through to _evaluate_authenticated (per-user counter keyed by uid, NAT-safe). Interim measure until APP_ACCESS_KEYs per tenant org are introduced. Co-Authored-By: Claude Sonnet 4.6 --- sapl/middleware/ratelimit.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/sapl/middleware/ratelimit.py b/sapl/middleware/ratelimit.py index 6d65f17b7..c44738abf 100644 --- a/sapl/middleware/ratelimit.py +++ b/sapl/middleware/ratelimit.py @@ -3,6 +3,8 @@ RateLimitMiddleware — cross-pod rate limiting backed by shared Redis. Decision flow (per request): 0. /api/ path AND consumer daily/weekly quota exceeded? → 429 + Anonymous /api/ (quota not exceeded): pass immediately — no IP counter, + no block key. nginx sapl_api zone (60r/m) is the burst gate. 1. Known bot UA? → 429 (Python list — substring match) 1b. Redis UA deny list? → 429 (runtime SET — token hash match, refreshed every 60 s) 2. Anonymous AND IP in blocked set? → 429 (authenticated users skip — have per-user limit at 3c) @@ -298,6 +300,16 @@ class RateLimitMiddleware: response['X-RateLimit-Reason'] = f'quota_{exceeded}' return response + # Anonymous /api/ requests: quota + nginx sapl_api zone are the only + # controls. Skip _evaluate so anonymous API traffic never increments + # the global IP counter or writes a block key — a misbehaving script + # behind a NAT must not lock out the org's page requests. + # Authenticated /api/ falls through to _evaluate_authenticated normally + # (per-user counter, NAT-safe). + user = getattr(request, 'user', None) + if not (user and user.is_authenticated): + return self.get_response(request) + decision = self._evaluate(request) if decision['action'] == 'block': logger.warning(