From d19023a1ba405f648ca43573cd6c3c46a01ec9d5 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Tue, 10 Mar 2026 20:37:24 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B(backend)=20refactor=20lobby=20thro?= =?UTF-8?q?ttling=20to=20use=20participant=20id=20instead=20of=20IP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit use the lobby participant cookie ID as the throttle cache key rather than the client IP address. This avoids penalising multiple users behind the same NAT or proxy and aligns throttling with how LobbyService identifies participants. This bug was spotted in production where users from ministries behind NAT was blocked by throttling while using visio. If no cookie is present yet, skip throttling for the request. The cookie will be set on the first response and throttling will apply from subsequent requests. This throttle is intended to protect against accidental hammering from buggy clients, not as a security control against DoS attacks. This is not a security measure, we should use a WAF. --- CHANGELOG.md | 1 + src/backend/core/api/throttling.py | 31 ++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cdab9c05..b68cb121 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ and this project adheres to - 🐛(frontend) fix hand icon and queue position alignment and position #1119 - 🩹(backend) add page_size to pagination for room endpoints #1131 +- 🐛(backend) refactor lobby throttling using participant id instead of IP #1129 ## [1.10.0] - 2026-03-05 diff --git a/src/backend/core/api/throttling.py b/src/backend/core/api/throttling.py index b6ec0c72..ecd4304b 100644 --- a/src/backend/core/api/throttling.py +++ b/src/backend/core/api/throttling.py @@ -1,5 +1,7 @@ """Throttling modules for the API.""" +from django.conf import settings + from lasuite.drf.throttling import MonitoredThrottleMixin from rest_framework.throttling import AnonRateThrottle from sentry_sdk import capture_message @@ -19,6 +21,35 @@ class RequestEntryAnonRateThrottle(MonitoredAnonRateThrottle): scope = "request_entry" + def get_cache_key(self, request, view): + """Use the lobby participant cookie ID as the throttle cache key. + + Only throttle if a cookie is already set. If no cookie exists yet, + return None to skip throttling — the cookie will be set on the first + response, and throttling will apply from the second request onward. + + Keying on the cookie rather than the IP address prevents penalising + multiple users behind the same NAT/proxy, and is consistent with how + LobbyService identifies participants. + + Note: as per DRF documentation, application-level throttling is not a + security measure against brute-force or DoS attacks. This throttle exists + solely to guard against accidental hammering from buggy clients. + """ + + if request.user and request.user.is_authenticated: + return None # Only throttle unauthenticated requests. + + participant_id = request.COOKIES.get(settings.LOBBY_COOKIE_NAME) + + if participant_id is None: + return None # No throttling for cookieless requests + + return self.cache_format % { + "scope": self.scope, + "ident": participant_id, + } + class CreationCallbackAnonRateThrottle(MonitoredAnonRateThrottle): """Throttle Anonymous user requesting room generation callback"""