rate_limit: Rate limit the /accounts/find/ endpoint.

Closes #19287

This endpoint allows submitting multiple addresses so we need to "weigh"
the rate limit more heavily the more emails are submitted. Clearly e.g.
a request triggering emails to 2 addresses should weigh twice as much as
a request doing that for just 1 address.
This commit is contained in:
Mateusz Mandera 2021-08-05 11:13:22 +02:00 committed by Mateusz Mandera
parent 29b3e81dd4
commit ddcfd9e2ee
4 changed files with 45 additions and 0 deletions

View File

@ -200,6 +200,36 @@ class RateLimitTests(ZulipTestCase):
finally: finally:
remove_ratelimit_rule(1, 5, domain="create_realm_by_ip") remove_ratelimit_rule(1, 5, domain="create_realm_by_ip")
def test_find_account_rate_limiting(self) -> None:
def assert_func(result: HttpResponse) -> None:
self.assertEqual(result.status_code, 429)
self.assert_in_response("Rate limit exceeded.", result)
add_ratelimit_rule(1, 5, domain="find_account_by_ip")
try:
RateLimitedIPAddr("127.0.0.1", domain="find_account_by_ip").clear_history()
self.do_test_hit_ratelimits(
lambda: self.client_post("/accounts/find/", {"emails": "new@zulip.com"}),
assert_func=assert_func,
)
finally:
remove_ratelimit_rule(1, 5, domain="find_account_by_ip")
# Now test whether submitting multiple emails is handled correctly.
# The limit is set to 10 per second, so 5 requests with 2 emails
# submitted in each should be allowed.
add_ratelimit_rule(1, 10, domain="find_account_by_ip")
try:
RateLimitedIPAddr("127.0.0.1", domain="find_account_by_ip").clear_history()
self.do_test_hit_ratelimits(
lambda: self.client_post(
"/accounts/find/", {"emails": "new@zulip.com,new2@zulip.com"}
),
assert_func=assert_func,
)
finally:
remove_ratelimit_rule(1, 10, domain="find_account_by_ip")
@skipUnless(settings.ZILENCER_ENABLED, "requires zilencer") @skipUnless(settings.ZILENCER_ENABLED, "requires zilencer")
def test_hit_ratelimits_as_remote_server(self) -> None: def test_hit_ratelimits_as_remote_server(self) -> None:
add_ratelimit_rule(1, 5, domain="api_by_remote_server") add_ratelimit_rule(1, 5, domain="api_by_remote_server")

View File

@ -711,6 +711,17 @@ def find_account(request: HttpRequest) -> HttpResponse:
form = FindMyTeamForm(request.POST) form = FindMyTeamForm(request.POST)
if form.is_valid(): if form.is_valid():
emails = form.cleaned_data["emails"] emails = form.cleaned_data["emails"]
for i in range(len(emails)):
try:
rate_limit_request_by_ip(request, domain="find_account_by_ip")
except RateLimited as e:
assert e.secs_to_freedom is not None
return render(
request,
"zerver/rate_limit_exceeded.html",
context={"retry_after": int(e.secs_to_freedom)},
status=429,
)
# Django doesn't support __iexact__in lookup with EmailField, so we have # Django doesn't support __iexact__in lookup with EmailField, so we have
# to use Qs to get around that without needing to do multiple queries. # to use Qs to get around that without needing to do multiple queries.

View File

@ -387,6 +387,9 @@ RATE_LIMITING_RULES = {
"create_realm_by_ip": [ "create_realm_by_ip": [
(1800, 5), (1800, 5),
], ],
"find_account_by_ip": [
(3600, 10),
],
"password_reset_form_by_email": [ "password_reset_form_by_email": [
(3600, 2), # 2 reset emails per hour (3600, 2), # 2 reset emails per hour
(86400, 5), # 5 per day (86400, 5), # 5 per day

View File

@ -265,6 +265,7 @@ RATE_LIMITING_RULES: Dict[str, List[Tuple[int, int]]] = {
"api_by_remote_server": [], "api_by_remote_server": [],
"authenticate_by_username": [], "authenticate_by_username": [],
"create_realm_by_ip": [], "create_realm_by_ip": [],
"find_account_by_ip": [],
"password_reset_form_by_email": [], "password_reset_form_by_email": [],
} }