Skip to content

Commit a683028

Browse files
authored
Correctly ratelimit invites when creating a room (matrix-org#9968)
* Correctly ratelimit invites when creating a room Also allow ratelimiting for more than one action at a time.
1 parent 7562d88 commit a683028

File tree

6 files changed

+157
-12
lines changed

6 files changed

+157
-12
lines changed

changelog.d/9968.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix a bug introduced in v1.27.0 preventing users and appservices exempt from ratelimiting from creating rooms with many invitees.

synapse/api/ratelimiting.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ async def can_do_action(
5757
rate_hz: Optional[float] = None,
5858
burst_count: Optional[int] = None,
5959
update: bool = True,
60+
n_actions: int = 1,
6061
_time_now_s: Optional[int] = None,
6162
) -> Tuple[bool, float]:
6263
"""Can the entity (e.g. user or IP address) perform the action?
@@ -76,6 +77,9 @@ async def can_do_action(
7677
burst_count: How many actions that can be performed before being limited.
7778
Overrides the value set during instantiation if set.
7879
update: Whether to count this check as performing the action
80+
n_actions: The number of times the user wants to do this action. If the user
81+
cannot do all of the actions, the user's action count is not incremented
82+
at all.
7983
_time_now_s: The current time. Optional, defaults to the current time according
8084
to self.clock. Only used by tests.
8185
@@ -124,17 +128,20 @@ async def can_do_action(
124128
time_delta = time_now_s - time_start
125129
performed_count = action_count - time_delta * rate_hz
126130
if performed_count < 0:
127-
# Allow, reset back to count 1
128-
allowed = True
131+
performed_count = 0
129132
time_start = time_now_s
130-
action_count = 1.0
131-
elif performed_count > burst_count - 1.0:
133+
134+
# This check would be easier read as performed_count + n_actions > burst_count,
135+
# but performed_count might be a very precise float (with lots of numbers
136+
# following the point) in which case Python might round it up when adding it to
137+
# n_actions. Writing it this way ensures it doesn't happen.
138+
if performed_count > burst_count - n_actions:
132139
# Deny, we have exceeded our burst count
133140
allowed = False
134141
else:
135142
# We haven't reached our limit yet
136143
allowed = True
137-
action_count += 1.0
144+
action_count = performed_count + n_actions
138145

139146
if update:
140147
self.actions[key] = (action_count, time_start, rate_hz)
@@ -182,6 +189,7 @@ async def ratelimit(
182189
rate_hz: Optional[float] = None,
183190
burst_count: Optional[int] = None,
184191
update: bool = True,
192+
n_actions: int = 1,
185193
_time_now_s: Optional[int] = None,
186194
):
187195
"""Checks if an action can be performed. If not, raises a LimitExceededError
@@ -201,6 +209,9 @@ async def ratelimit(
201209
burst_count: How many actions that can be performed before being limited.
202210
Overrides the value set during instantiation if set.
203211
update: Whether to count this check as performing the action
212+
n_actions: The number of times the user wants to do this action. If the user
213+
cannot do all of the actions, the user's action count is not incremented
214+
at all.
204215
_time_now_s: The current time. Optional, defaults to the current time according
205216
to self.clock. Only used by tests.
206217
@@ -216,6 +227,7 @@ async def ratelimit(
216227
rate_hz=rate_hz,
217228
burst_count=burst_count,
218229
update=update,
230+
n_actions=n_actions,
219231
_time_now_s=time_now_s,
220232
)
221233

synapse/handlers/room.py

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,14 @@
3232
RoomCreationPreset,
3333
RoomEncryptionAlgorithms,
3434
)
35-
from synapse.api.errors import AuthError, Codes, NotFoundError, StoreError, SynapseError
35+
from synapse.api.errors import (
36+
AuthError,
37+
Codes,
38+
LimitExceededError,
39+
NotFoundError,
40+
StoreError,
41+
SynapseError,
42+
)
3643
from synapse.api.filtering import Filter
3744
from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersion
3845
from synapse.events import EventBase
@@ -126,10 +133,6 @@ def __init__(self, hs: "HomeServer"):
126133

127134
self.third_party_event_rules = hs.get_third_party_event_rules()
128135

129-
self._invite_burst_count = (
130-
hs.config.ratelimiting.rc_invites_per_room.burst_count
131-
)
132-
133136
async def upgrade_room(
134137
self, requester: Requester, old_room_id: str, new_version: RoomVersion
135138
) -> str:
@@ -676,8 +679,18 @@ async def create_room(
676679
invite_3pid_list = []
677680
invite_list = []
678681

679-
if len(invite_list) + len(invite_3pid_list) > self._invite_burst_count:
680-
raise SynapseError(400, "Cannot invite so many users at once")
682+
if invite_list or invite_3pid_list:
683+
try:
684+
# If there are invites in the request, see if the ratelimiting settings
685+
# allow that number of invites to be sent from the current user.
686+
await self.room_member_handler.ratelimit_multiple_invites(
687+
requester,
688+
room_id=None,
689+
n_invites=len(invite_list) + len(invite_3pid_list),
690+
update=False,
691+
)
692+
except LimitExceededError:
693+
raise SynapseError(400, "Cannot invite so many users at once")
681694

682695
await self.event_creation_handler.assert_accepted_privacy_policy(requester)
683696

synapse/handlers/room_member.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,31 @@ async def _user_left_room(self, target: UserID, room_id: str) -> None:
163163
async def forget(self, user: UserID, room_id: str) -> None:
164164
raise NotImplementedError()
165165

166+
async def ratelimit_multiple_invites(
167+
self,
168+
requester: Optional[Requester],
169+
room_id: Optional[str],
170+
n_invites: int,
171+
update: bool = True,
172+
):
173+
"""Ratelimit more than one invite sent by the given requester in the given room.
174+
175+
Args:
176+
requester: The requester sending the invites.
177+
room_id: The room the invites are being sent in.
178+
n_invites: The amount of invites to ratelimit for.
179+
update: Whether to update the ratelimiter's cache.
180+
181+
Raises:
182+
LimitExceededError: The requester can't send that many invites in the room.
183+
"""
184+
await self._invites_per_room_limiter.ratelimit(
185+
requester,
186+
room_id,
187+
update=update,
188+
n_actions=n_invites,
189+
)
190+
166191
async def ratelimit_invite(
167192
self,
168193
requester: Optional[Requester],

tests/api/test_ratelimiting.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,3 +230,60 @@ def test_db_user_override(self):
230230
# Shouldn't raise
231231
for _ in range(20):
232232
self.get_success_or_raise(limiter.ratelimit(requester, _time_now_s=0))
233+
234+
def test_multiple_actions(self):
235+
limiter = Ratelimiter(
236+
store=self.hs.get_datastore(), clock=None, rate_hz=0.1, burst_count=3
237+
)
238+
# Test that 4 actions aren't allowed with a maximum burst of 3.
239+
allowed, time_allowed = self.get_success_or_raise(
240+
limiter.can_do_action(None, key="test_id", n_actions=4, _time_now_s=0)
241+
)
242+
self.assertFalse(allowed)
243+
244+
# Test that 3 actions are allowed with a maximum burst of 3.
245+
allowed, time_allowed = self.get_success_or_raise(
246+
limiter.can_do_action(None, key="test_id", n_actions=3, _time_now_s=0)
247+
)
248+
self.assertTrue(allowed)
249+
self.assertEquals(10.0, time_allowed)
250+
251+
# Test that, after doing these 3 actions, we can't do any more action without
252+
# waiting.
253+
allowed, time_allowed = self.get_success_or_raise(
254+
limiter.can_do_action(None, key="test_id", n_actions=1, _time_now_s=0)
255+
)
256+
self.assertFalse(allowed)
257+
self.assertEquals(10.0, time_allowed)
258+
259+
# Test that after waiting we can do only 1 action.
260+
allowed, time_allowed = self.get_success_or_raise(
261+
limiter.can_do_action(
262+
None,
263+
key="test_id",
264+
update=False,
265+
n_actions=1,
266+
_time_now_s=10,
267+
)
268+
)
269+
self.assertTrue(allowed)
270+
# The time allowed is the current time because we could still repeat the action
271+
# once.
272+
self.assertEquals(10.0, time_allowed)
273+
274+
allowed, time_allowed = self.get_success_or_raise(
275+
limiter.can_do_action(None, key="test_id", n_actions=2, _time_now_s=10)
276+
)
277+
self.assertFalse(allowed)
278+
# The time allowed doesn't change despite allowed being False because, while we
279+
# don't allow 2 actions, we could still do 1.
280+
self.assertEquals(10.0, time_allowed)
281+
282+
# Test that after waiting a bit more we can do 2 actions.
283+
allowed, time_allowed = self.get_success_or_raise(
284+
limiter.can_do_action(None, key="test_id", n_actions=2, _time_now_s=20)
285+
)
286+
self.assertTrue(allowed)
287+
# The time allowed is the current time because we could still repeat the action
288+
# once.
289+
self.assertEquals(20.0, time_allowed)

tests/rest/client/v1/test_rooms.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,43 @@ def test_post_room_invitees_invalid_mxid(self):
463463
)
464464
self.assertEquals(400, channel.code)
465465

466+
@unittest.override_config({"rc_invites": {"per_room": {"burst_count": 3}}})
467+
def test_post_room_invitees_ratelimit(self):
468+
"""Test that invites sent when creating a room are ratelimited by a RateLimiter,
469+
which ratelimits them correctly, including by not limiting when the requester is
470+
exempt from ratelimiting.
471+
"""
472+
473+
# Build the request's content. We use local MXIDs because invites over federation
474+
# are more difficult to mock.
475+
content = json.dumps(
476+
{
477+
"invite": [
478+
"@alice1:red",
479+
"@alice2:red",
480+
"@alice3:red",
481+
"@alice4:red",
482+
]
483+
}
484+
).encode("utf8")
485+
486+
# Test that the invites are correctly ratelimited.
487+
channel = self.make_request("POST", "/createRoom", content)
488+
self.assertEqual(400, channel.code)
489+
self.assertEqual(
490+
"Cannot invite so many users at once",
491+
channel.json_body["error"],
492+
)
493+
494+
# Add the current user to the ratelimit overrides, allowing them no ratelimiting.
495+
self.get_success(
496+
self.hs.get_datastore().set_ratelimit_for_user(self.user_id, 0, 0)
497+
)
498+
499+
# Test that the invites aren't ratelimited anymore.
500+
channel = self.make_request("POST", "/createRoom", content)
501+
self.assertEqual(200, channel.code)
502+
466503

467504
class RoomTopicTestCase(RoomBase):
468505
""" Tests /rooms/$room_id/topic REST events. """

0 commit comments

Comments
 (0)