diff --git a/sapl/middleware/ratelimit.py b/sapl/middleware/ratelimit.py index 84da8802b..b96729f97 100644 --- a/sapl/middleware/ratelimit.py +++ b/sapl/middleware/ratelimit.py @@ -51,11 +51,31 @@ def _sha256(s): return hashlib.sha256(s.encode()).hexdigest() -def _get_ip(request): - return ( - request.META.get('HTTP_X_FORWARDED_FOR', '').split(',')[0].strip() - or request.META.get('REMOTE_ADDR', '') - ) +def get_client_ip(request): + """ + Return the real client IP, applying django-ratelimit's ip_mask so that + IPv6 /64 subnets are collapsed to a single key (prevents per-address + rotation attacks). Also checks HTTP_X_REAL_IP for nginx setups that + use that header instead of X-Forwarded-For. + + Canonical source — imported from here by other SAPL modules. + """ + from ratelimit.core import ip_mask + x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') + if x_forwarded_for: + ip = x_forwarded_for.split(',')[0].strip() + else: + ip = ( + request.META.get('HTTP_X_REAL_IP') + or request.META.get('REMOTE_ADDR') + or '0.0.0.0' + ) + return ip_mask(ip) + + +def ratelimit_ip(group, request): + """Key function for django-ratelimit decorators (group param is ignored).""" + return get_client_ip(request) def _is_suspicious_headers(request): @@ -80,17 +100,10 @@ class RateLimitMiddleware: def __init__(self, get_response): self.get_response = get_response - self.dry_run = getattr(settings, 'RATELIMIT_DRY_RUN', True) - - anon_rate = getattr(settings, 'RATE_LIMITER_RATE', '35/m') - auth_rate = getattr(settings, 'RATE_LIMITER_RATE_AUTHENTICATED', '120/m') - - self.anon_threshold, self.anon_window = _parse_rate(anon_rate) - self.auth_threshold, self.auth_window = _parse_rate(auth_rate) - - self.whitelist = set( - getattr(settings, 'RATE_LIMIT_WHITELIST_IPS', []) or [] - ) + self.dry_run = settings.RATELIMIT_DRY_RUN + self.anon_threshold, self.anon_window = _parse_rate(settings.RATE_LIMITER_RATE) + self.auth_threshold, self.auth_window = _parse_rate(settings.RATE_LIMITER_RATE_AUTHENTICATED) + self.whitelist = set(settings.RATE_LIMIT_WHITELIST_IPS) self._rl_cache = caches['ratelimit'] def __call__(self, request): @@ -116,7 +129,7 @@ class RateLimitMiddleware: # ------------------------------------------------------------------ def _evaluate(self, request): - ip = _get_ip(request) + ip = get_client_ip(request) if ip in self.whitelist: return {'action': 'pass', 'ip': ip} diff --git a/sapl/utils.py b/sapl/utils.py index ee97094aa..1b7ce99c0 100644 --- a/sapl/utils.py +++ b/sapl/utils.py @@ -401,21 +401,9 @@ def xstr(s): return '' if s is None else str(s) -def get_client_ip(request): - from ratelimit.core import ip_mask - x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') - if x_forwarded_for: - ip = x_forwarded_for.split(',')[0] - else: - ip = request.META.get('HTTP_X_REAL_IP') or request.META.get('REMOTE_ADDR') or '0.0.0.0' - return ip_mask(ip) - - -def ratelimit_ip(group, request): - """ - Ignore group param in django-ratelimit==3.0.1 - """ - return get_client_ip(request) +# Canonical implementations live in sapl.middleware.ratelimit. +# Re-exported here so existing import sites keep working unchanged. +from sapl.middleware.ratelimit import get_client_ip, ratelimit_ip # noqa: F401, E402 def get_base_url(request):