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

Correctly ratelimit invites when creating a room #9968

Merged
merged 11 commits into from
May 12, 2021
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/9968.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix a bug introduced in v1.27.0 preventing users and appservices exempt from ratelimiting from creating rooms with many invitees.
16 changes: 11 additions & 5 deletions synapse/api/ratelimiting.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ async def can_do_action(
rate_hz: Optional[float] = None,
burst_count: Optional[int] = None,
update: bool = True,
nb_actions: int = 1,
_time_now_s: Optional[int] = None,
) -> Tuple[bool, float]:
"""Can the entity (e.g. user or IP address) perform the action?
Expand All @@ -76,6 +77,7 @@ async def can_do_action(
burst_count: How many actions that can be performed before being limited.
Overrides the value set during instantiation if set.
update: Whether to count this check as performing the action
nb_actions: The number of actions the user wants to do.
babolivier marked this conversation as resolved.
Show resolved Hide resolved
babolivier marked this conversation as resolved.
Show resolved Hide resolved
_time_now_s: The current time. Optional, defaults to the current time according
to self.clock. Only used by tests.

Expand Down Expand Up @@ -124,17 +126,18 @@ async def can_do_action(
time_delta = time_now_s - time_start
performed_count = action_count - time_delta * rate_hz
if performed_count < 0:
# Allow, reset back to count 1
allowed = True
# Whether the actions are allowed depends if performing them exceeds the
# burst count.
allowed = nb_actions <= burst_count
time_start = time_now_s
action_count = 1.0
elif performed_count > burst_count - 1.0:
action_count = nb_actions
babolivier marked this conversation as resolved.
Show resolved Hide resolved
elif performed_count + nb_actions > burst_count:
# Deny, we have exceeded our burst count
allowed = False
else:
# We haven't reached our limit yet
allowed = True
action_count += 1.0
action_count += nb_actions

if update:
self.actions[key] = (action_count, time_start, rate_hz)
Expand Down Expand Up @@ -182,6 +185,7 @@ async def ratelimit(
rate_hz: Optional[float] = None,
burst_count: Optional[int] = None,
update: bool = True,
nb_actions: int = 1,
_time_now_s: Optional[int] = None,
):
"""Checks if an action can be performed. If not, raises a LimitExceededError
Expand All @@ -201,6 +205,7 @@ async def ratelimit(
burst_count: How many actions that can be performed before being limited.
Overrides the value set during instantiation if set.
update: Whether to count this check as performing the action
nb_actions: The number of actions the user wants to do.
_time_now_s: The current time. Optional, defaults to the current time according
to self.clock. Only used by tests.

Expand All @@ -216,6 +221,7 @@ async def ratelimit(
rate_hz=rate_hz,
burst_count=burst_count,
update=update,
nb_actions=nb_actions,
_time_now_s=time_now_s,
)

Expand Down
27 changes: 19 additions & 8 deletions synapse/handlers/room.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,14 @@
RoomCreationPreset,
RoomEncryptionAlgorithms,
)
from synapse.api.errors import AuthError, Codes, NotFoundError, StoreError, SynapseError
from synapse.api.errors import (
AuthError,
Codes,
LimitExceededError,
NotFoundError,
StoreError,
SynapseError,
)
from synapse.api.filtering import Filter
from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersion
from synapse.events import EventBase
Expand Down Expand Up @@ -126,10 +133,6 @@ def __init__(self, hs: "HomeServer"):

self.third_party_event_rules = hs.get_third_party_event_rules()

self._invite_burst_count = (
hs.config.ratelimiting.rc_invites_per_room.burst_count
)

async def upgrade_room(
self, requester: Requester, old_room_id: str, new_version: RoomVersion
) -> str:
Expand Down Expand Up @@ -676,9 +679,6 @@ async def create_room(
invite_3pid_list = []
invite_list = []

if len(invite_list) + len(invite_3pid_list) > self._invite_burst_count:
raise SynapseError(400, "Cannot invite so many users at once")

await self.event_creation_handler.assert_accepted_privacy_policy(requester)

power_level_content_override = config.get("power_level_content_override")
Expand All @@ -702,6 +702,17 @@ async def create_room(
room_version=room_version,
)

if invite_list or invite_3pid_list:
try:
await self.room_member_handler.ratelimit_multiple_invites(
requester,
room_id,
nb_invites=len(invite_list) + len(invite_3pid_list),
update=False,
)
except LimitExceededError:
raise SynapseError(400, "Cannot invite so many users at once")

babolivier marked this conversation as resolved.
Show resolved Hide resolved
# Check whether this visibility value is blocked by a third party module
allowed_by_third_party_rules = await (
self.third_party_event_rules.check_visibility_can_be_modified(
Expand Down
25 changes: 25 additions & 0 deletions synapse/handlers/room_member.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,31 @@ async def _user_left_room(self, target: UserID, room_id: str) -> None:
async def forget(self, user: UserID, room_id: str) -> None:
raise NotImplementedError()

async def ratelimit_multiple_invites(
self,
requester: Optional[Requester],
room_id: str,
nb_invites: int,
update: bool = True,
):
"""Ratelimit more than one invite sent by the given requester in the given room.

Args:
requester: The requester sending the invites.
room_id: The room the invites are being sent in.
nb_invites: The amount of invites to ratelimit for.
update: Whether to update the ratelimiter's cache.

Raises:
LimitExceededError: The requester can't send that many invites in the room.
"""
await self._invites_per_room_limiter.ratelimit(
requester,
room_id,
update=update,
nb_actions=nb_invites,
)

async def ratelimit_invite(
self,
requester: Optional[Requester],
Expand Down
37 changes: 37 additions & 0 deletions tests/rest/client/v1/test_rooms.py
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,43 @@ def test_post_room_invitees_invalid_mxid(self):
)
self.assertEquals(400, channel.code)

@unittest.override_config({"rc_invites": {"per_room": {"burst_count": 3}}})
def test_post_room_invitees_ratelimit(self):
"""Test that invites sent when creating a room are ratelimited by a RateLimiter,
which ratelimits them correctly, including by not limiting when the requester is
exempt from ratelimiting.
"""

# Build the request's content. We use local MXIDs because invites over federation
# are more difficult to mock.
content = json.dumps(
{
"invite": [
"@alice1:red",
"@alice2:red",
"@alice3:red",
"@alice4:red",
]
}
).encode("utf8")

# Test that the invites are correctly ratelimited.
channel = self.make_request("POST", "/createRoom", content)
self.assertEqual(400, channel.code)
self.assertEqual(
"Cannot invite so many users at once",
channel.json_body["error"],
)

# Add the current user to the ratelimit overrides, allowing them no ratelimiting.
self.get_success(
self.hs.get_datastore().set_ratelimit_for_user(self.user_id, 0, 0)
)

# Test that the invites aren't ratelimited anymore.
channel = self.make_request("POST", "/createRoom", content)
self.assertEqual(200, channel.code)


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