Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit 0dbdc39

Browse files
authored
Fix a long-standing bug which meant that rate limiting was not restrictive enough in some cases. (#13018)
1 parent 417f4cf commit 0dbdc39

File tree

3 files changed

+45
-12
lines changed

3 files changed

+45
-12
lines changed

changelog.d/13018.bugfix

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix a long-standing bug which meant that rate limiting was not restrictive enough in some cases.

synapse/api/ratelimiting.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,9 @@ async def can_do_action(
128128
performed_count = action_count - time_delta * rate_hz
129129
if performed_count < 0:
130130
performed_count = 0
131+
132+
# Reset the start time and forgive all actions
133+
action_count = 0
131134
time_start = time_now_s
132135

133136
# This check would be easier read as performed_count + n_actions > burst_count,
@@ -140,7 +143,7 @@ async def can_do_action(
140143
else:
141144
# We haven't reached our limit yet
142145
allowed = True
143-
action_count = performed_count + n_actions
146+
action_count = action_count + n_actions
144147

145148
if update:
146149
self.actions[key] = (action_count, time_start, rate_hz)

tests/api/test_ratelimiting.py

+40-11
Original file line numberDiff line numberDiff line change
@@ -246,15 +246,16 @@ def test_multiple_actions(self):
246246
self.assertTrue(allowed)
247247
self.assertEqual(10.0, time_allowed)
248248

249-
# Test that, after doing these 3 actions, we can't do any more action without
249+
# Test that, after doing these 3 actions, we can't do any more actions without
250250
# waiting.
251251
allowed, time_allowed = self.get_success_or_raise(
252252
limiter.can_do_action(None, key="test_id", n_actions=1, _time_now_s=0)
253253
)
254254
self.assertFalse(allowed)
255255
self.assertEqual(10.0, time_allowed)
256256

257-
# Test that after waiting we can do only 1 action.
257+
# Test that after waiting we would be able to do only 1 action.
258+
# Note that we don't actually do it (update=False) here.
258259
allowed, time_allowed = self.get_success_or_raise(
259260
limiter.can_do_action(
260261
None,
@@ -265,23 +266,51 @@ def test_multiple_actions(self):
265266
)
266267
)
267268
self.assertTrue(allowed)
268-
# The time allowed is the current time because we could still repeat the action
269-
# once.
270-
self.assertEqual(10.0, time_allowed)
269+
# We would be able to do the 5th action at t=20.
270+
self.assertEqual(20.0, time_allowed)
271271

272+
# Attempt (but fail) to perform TWO actions at t=10.
273+
# Those would be the 4th and 5th actions.
272274
allowed, time_allowed = self.get_success_or_raise(
273275
limiter.can_do_action(None, key="test_id", n_actions=2, _time_now_s=10)
274276
)
275277
self.assertFalse(allowed)
276-
# The time allowed doesn't change despite allowed being False because, while we
277-
# don't allow 2 actions, we could still do 1.
278+
# The returned time allowed for the next action is now even though we weren't
279+
# allowed to perform the action because whilst we don't allow 2 actions,
280+
# we could still do 1.
278281
self.assertEqual(10.0, time_allowed)
279282

280-
# Test that after waiting a bit more we can do 2 actions.
283+
# Test that after waiting until t=20, we can do perform 2 actions.
284+
# These are the 4th and 5th actions.
281285
allowed, time_allowed = self.get_success_or_raise(
282286
limiter.can_do_action(None, key="test_id", n_actions=2, _time_now_s=20)
283287
)
284288
self.assertTrue(allowed)
285-
# The time allowed is the current time because we could still repeat the action
286-
# once.
287-
self.assertEqual(20.0, time_allowed)
289+
# We would be able to do the 6th action at t=30.
290+
self.assertEqual(30.0, time_allowed)
291+
292+
def test_rate_limit_burst_only_given_once(self) -> None:
293+
"""
294+
Regression test against a bug that meant that you could build up
295+
extra tokens by timing requests.
296+
"""
297+
limiter = Ratelimiter(
298+
store=self.hs.get_datastores().main, clock=None, rate_hz=0.1, burst_count=3
299+
)
300+
301+
def consume_at(time: float) -> bool:
302+
success, _ = self.get_success_or_raise(
303+
limiter.can_do_action(requester=None, key="a", _time_now_s=time)
304+
)
305+
return success
306+
307+
# Use all our 3 burst tokens
308+
self.assertTrue(consume_at(0.0))
309+
self.assertTrue(consume_at(0.1))
310+
self.assertTrue(consume_at(0.2))
311+
312+
# Wait to recover 1 token (10 seconds at 0.1 Hz).
313+
self.assertTrue(consume_at(10.1))
314+
315+
# Check that we get rate limited after using that token.
316+
self.assertFalse(consume_at(11.1))

0 commit comments

Comments
 (0)