diff --git a/app/api/routes.py b/app/api/routes.py index e41b505..0deb05b 100644 --- a/app/api/routes.py +++ b/app/api/routes.py @@ -339,11 +339,33 @@ def check_lookup_rate_limit(client_ip: str) -> tuple[bool, int]: window = current_app.config.get("LOOKUP_RATE_LIMIT_WINDOW", 60) max_requests = current_app.config.get("LOOKUP_RATE_LIMIT_MAX", 60) + max_entries = current_app.config.get("LOOKUP_RATE_LIMIT_MAX_ENTRIES", 10000) now = time.time() cutoff = now - window with _lookup_rate_limit_lock: + # ENUM-002: Memory protection - prune if at capacity + if len(_lookup_rate_limit_requests) >= max_entries: + if client_ip not in _lookup_rate_limit_requests: + # Evict expired entries first + expired = [ + ip + for ip, reqs in _lookup_rate_limit_requests.items() + if not reqs or reqs[-1] <= cutoff + ] + for ip in expired: + del _lookup_rate_limit_requests[ip] + + # If still at capacity, evict oldest entries + if len(_lookup_rate_limit_requests) >= max_entries: + sorted_ips = sorted( + _lookup_rate_limit_requests.items(), + key=lambda x: x[1][-1] if x[1] else 0, + ) + for ip, _ in sorted_ips[: max_entries // 4]: + del _lookup_rate_limit_requests[ip] + requests = _lookup_rate_limit_requests[client_ip] requests[:] = [t for t in requests if t > cutoff] diff --git a/app/config.py b/app/config.py index 0725662..9f93765 100644 --- a/app/config.py +++ b/app/config.py @@ -115,6 +115,10 @@ class Config: ) LOOKUP_RATE_LIMIT_WINDOW = int(os.environ.get("FLASKPASTE_LOOKUP_RATE_WINDOW", "60")) LOOKUP_RATE_LIMIT_MAX = int(os.environ.get("FLASKPASTE_LOOKUP_RATE_MAX", "60")) + # ENUM-002: Maximum tracked IPs for lookup rate limiting (memory protection) + LOOKUP_RATE_LIMIT_MAX_ENTRIES = int( + os.environ.get("FLASKPASTE_LOOKUP_RATE_MAX_ENTRIES", "10000") + ) # Audit Logging # Track security-relevant events (paste creation, deletion, rate limits, etc.)