mirror of
https://mau.dev/maunium/synapse.git
synced 2024-12-14 08:03:47 +01:00
Ratelimit invites by room and target user (#9258)
This commit is contained in:
parent
e19396d622
commit
f2c1560eca
9 changed files with 192 additions and 4 deletions
1
changelog.d/9258.feature
Normal file
1
changelog.d/9258.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Add ratelimits to invites in rooms and to specific users.
|
|
@ -825,6 +825,8 @@ log_config: "CONFDIR/SERVERNAME.log.config"
|
|||
# "remote" for when users are trying to join rooms not on the server (which
|
||||
# can be more expensive)
|
||||
# - one for ratelimiting how often a user or IP can attempt to validate a 3PID.
|
||||
# - two for ratelimiting how often invites can be sent in a room or to a
|
||||
# specific user.
|
||||
#
|
||||
# The defaults are as shown below.
|
||||
#
|
||||
|
@ -862,6 +864,14 @@ log_config: "CONFDIR/SERVERNAME.log.config"
|
|||
#rc_3pid_validation:
|
||||
# per_second: 0.003
|
||||
# burst_count: 5
|
||||
#
|
||||
#rc_invites:
|
||||
# per_room:
|
||||
# per_second: 0.3
|
||||
# burst_count: 10
|
||||
# per_user:
|
||||
# per_second: 0.003
|
||||
# burst_count: 5
|
||||
|
||||
# Ratelimiting settings for incoming federation
|
||||
#
|
||||
|
|
|
@ -107,6 +107,15 @@ class RatelimitConfig(Config):
|
|||
defaults={"per_second": 0.003, "burst_count": 5},
|
||||
)
|
||||
|
||||
self.rc_invites_per_room = RateLimitConfig(
|
||||
config.get("rc_invites", {}).get("per_room", {}),
|
||||
defaults={"per_second": 0.3, "burst_count": 10},
|
||||
)
|
||||
self.rc_invites_per_user = RateLimitConfig(
|
||||
config.get("rc_invites", {}).get("per_user", {}),
|
||||
defaults={"per_second": 0.003, "burst_count": 5},
|
||||
)
|
||||
|
||||
def generate_config_section(self, **kwargs):
|
||||
return """\
|
||||
## Ratelimiting ##
|
||||
|
@ -137,6 +146,8 @@ class RatelimitConfig(Config):
|
|||
# "remote" for when users are trying to join rooms not on the server (which
|
||||
# can be more expensive)
|
||||
# - one for ratelimiting how often a user or IP can attempt to validate a 3PID.
|
||||
# - two for ratelimiting how often invites can be sent in a room or to a
|
||||
# specific user.
|
||||
#
|
||||
# The defaults are as shown below.
|
||||
#
|
||||
|
@ -174,6 +185,14 @@ class RatelimitConfig(Config):
|
|||
#rc_3pid_validation:
|
||||
# per_second: 0.003
|
||||
# burst_count: 5
|
||||
#
|
||||
#rc_invites:
|
||||
# per_room:
|
||||
# per_second: 0.3
|
||||
# burst_count: 10
|
||||
# per_user:
|
||||
# per_second: 0.003
|
||||
# burst_count: 5
|
||||
|
||||
# Ratelimiting settings for incoming federation
|
||||
#
|
||||
|
|
|
@ -810,7 +810,7 @@ class FederationClient(FederationBase):
|
|||
"User's homeserver does not support this room version",
|
||||
Codes.UNSUPPORTED_ROOM_VERSION,
|
||||
)
|
||||
elif e.code == 403:
|
||||
elif e.code in (403, 429):
|
||||
raise e.to_synapse_error()
|
||||
else:
|
||||
raise
|
||||
|
|
|
@ -1617,6 +1617,10 @@ class FederationHandler(BaseHandler):
|
|||
if event.state_key == self._server_notices_mxid:
|
||||
raise SynapseError(HTTPStatus.FORBIDDEN, "Cannot invite this user")
|
||||
|
||||
# We retrieve the room member handler here as to not cause a cyclic dependency
|
||||
member_handler = self.hs.get_room_member_handler()
|
||||
member_handler.ratelimit_invite(event.room_id, event.state_key)
|
||||
|
||||
# keep a record of the room version, if we don't yet know it.
|
||||
# (this may get overwritten if we later get a different room version in a
|
||||
# join dance).
|
||||
|
|
|
@ -126,6 +126,10 @@ class RoomCreationHandler(BaseHandler):
|
|||
|
||||
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:
|
||||
|
@ -662,6 +666,9 @@ class RoomCreationHandler(BaseHandler):
|
|||
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")
|
||||
|
|
|
@ -85,6 +85,17 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
|
|||
burst_count=hs.config.ratelimiting.rc_joins_remote.burst_count,
|
||||
)
|
||||
|
||||
self._invites_per_room_limiter = Ratelimiter(
|
||||
clock=self.clock,
|
||||
rate_hz=hs.config.ratelimiting.rc_invites_per_room.per_second,
|
||||
burst_count=hs.config.ratelimiting.rc_invites_per_room.burst_count,
|
||||
)
|
||||
self._invites_per_user_limiter = Ratelimiter(
|
||||
clock=self.clock,
|
||||
rate_hz=hs.config.ratelimiting.rc_invites_per_user.per_second,
|
||||
burst_count=hs.config.ratelimiting.rc_invites_per_user.burst_count,
|
||||
)
|
||||
|
||||
# This is only used to get at ratelimit function, and
|
||||
# maybe_kick_guest_users. It's fine there are multiple of these as
|
||||
# it doesn't store state.
|
||||
|
@ -144,6 +155,12 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
|
|||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def ratelimit_invite(self, room_id: str, invitee_user_id: str):
|
||||
"""Ratelimit invites by room and by target user.
|
||||
"""
|
||||
self._invites_per_room_limiter.ratelimit(room_id)
|
||||
self._invites_per_user_limiter.ratelimit(invitee_user_id)
|
||||
|
||||
async def _local_membership_update(
|
||||
self,
|
||||
requester: Requester,
|
||||
|
@ -387,8 +404,12 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
|
|||
raise SynapseError(403, "This room has been blocked on this server")
|
||||
|
||||
if effective_membership_state == Membership.INVITE:
|
||||
target_id = target.to_string()
|
||||
if ratelimit:
|
||||
self.ratelimit_invite(room_id, target_id)
|
||||
|
||||
# block any attempts to invite the server notices mxid
|
||||
if target.to_string() == self._server_notices_mxid:
|
||||
if target_id == self._server_notices_mxid:
|
||||
raise SynapseError(HTTPStatus.FORBIDDEN, "Cannot invite this user")
|
||||
|
||||
block_invite = False
|
||||
|
@ -412,7 +433,7 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
|
|||
block_invite = True
|
||||
|
||||
if not await self.spam_checker.user_may_invite(
|
||||
requester.user.to_string(), target.to_string(), room_id
|
||||
requester.user.to_string(), target_id, room_id
|
||||
):
|
||||
logger.info("Blocking invite due to spam checker")
|
||||
block_invite = True
|
||||
|
|
|
@ -16,7 +16,7 @@ import logging
|
|||
from unittest import TestCase
|
||||
|
||||
from synapse.api.constants import EventTypes
|
||||
from synapse.api.errors import AuthError, Codes, SynapseError
|
||||
from synapse.api.errors import AuthError, Codes, LimitExceededError, SynapseError
|
||||
from synapse.api.room_versions import RoomVersions
|
||||
from synapse.events import EventBase
|
||||
from synapse.federation.federation_base import event_from_pdu_json
|
||||
|
@ -191,6 +191,97 @@ class FederationTestCase(unittest.HomeserverTestCase):
|
|||
|
||||
self.assertEqual(sg, sg2)
|
||||
|
||||
@unittest.override_config(
|
||||
{"rc_invites": {"per_room": {"per_second": 0.5, "burst_count": 3}}}
|
||||
)
|
||||
def test_invite_by_room_ratelimit(self):
|
||||
"""Tests that invites from federation in a room are actually rate-limited.
|
||||
"""
|
||||
other_server = "otherserver"
|
||||
other_user = "@otheruser:" + other_server
|
||||
|
||||
# create the room
|
||||
user_id = self.register_user("kermit", "test")
|
||||
tok = self.login("kermit", "test")
|
||||
room_id = self.helper.create_room_as(room_creator=user_id, tok=tok)
|
||||
room_version = self.get_success(self.store.get_room_version(room_id))
|
||||
|
||||
def create_invite_for(local_user):
|
||||
return event_from_pdu_json(
|
||||
{
|
||||
"type": EventTypes.Member,
|
||||
"content": {"membership": "invite"},
|
||||
"room_id": room_id,
|
||||
"sender": other_user,
|
||||
"state_key": local_user,
|
||||
"depth": 32,
|
||||
"prev_events": [],
|
||||
"auth_events": [],
|
||||
"origin_server_ts": self.clock.time_msec(),
|
||||
},
|
||||
room_version,
|
||||
)
|
||||
|
||||
for i in range(3):
|
||||
self.get_success(
|
||||
self.handler.on_invite_request(
|
||||
other_server,
|
||||
create_invite_for("@user-%d:test" % (i,)),
|
||||
room_version,
|
||||
)
|
||||
)
|
||||
|
||||
self.get_failure(
|
||||
self.handler.on_invite_request(
|
||||
other_server, create_invite_for("@user-4:test"), room_version,
|
||||
),
|
||||
exc=LimitExceededError,
|
||||
)
|
||||
|
||||
@unittest.override_config(
|
||||
{"rc_invites": {"per_user": {"per_second": 0.5, "burst_count": 3}}}
|
||||
)
|
||||
def test_invite_by_user_ratelimit(self):
|
||||
"""Tests that invites from federation to a particular user are
|
||||
actually rate-limited.
|
||||
"""
|
||||
other_server = "otherserver"
|
||||
other_user = "@otheruser:" + other_server
|
||||
|
||||
# create the room
|
||||
user_id = self.register_user("kermit", "test")
|
||||
tok = self.login("kermit", "test")
|
||||
|
||||
def create_invite():
|
||||
room_id = self.helper.create_room_as(room_creator=user_id, tok=tok)
|
||||
room_version = self.get_success(self.store.get_room_version(room_id))
|
||||
return event_from_pdu_json(
|
||||
{
|
||||
"type": EventTypes.Member,
|
||||
"content": {"membership": "invite"},
|
||||
"room_id": room_id,
|
||||
"sender": other_user,
|
||||
"state_key": "@user:test",
|
||||
"depth": 32,
|
||||
"prev_events": [],
|
||||
"auth_events": [],
|
||||
"origin_server_ts": self.clock.time_msec(),
|
||||
},
|
||||
room_version,
|
||||
)
|
||||
|
||||
for i in range(3):
|
||||
event = create_invite()
|
||||
self.get_success(
|
||||
self.handler.on_invite_request(other_server, event, event.room_version,)
|
||||
)
|
||||
|
||||
event = create_invite()
|
||||
self.get_failure(
|
||||
self.handler.on_invite_request(other_server, event, event.room_version,),
|
||||
exc=LimitExceededError,
|
||||
)
|
||||
|
||||
def _build_and_send_join_event(self, other_server, other_user, room_id):
|
||||
join_event = self.get_success(
|
||||
self.handler.on_make_join_request(other_server, room_id, other_user)
|
||||
|
|
|
@ -616,6 +616,41 @@ class RoomMemberStateTestCase(RoomBase):
|
|||
self.assertEquals(json.loads(content), channel.json_body)
|
||||
|
||||
|
||||
class RoomInviteRatelimitTestCase(RoomBase):
|
||||
user_id = "@sid1:red"
|
||||
|
||||
servlets = [
|
||||
admin.register_servlets,
|
||||
profile.register_servlets,
|
||||
room.register_servlets,
|
||||
]
|
||||
|
||||
@unittest.override_config(
|
||||
{"rc_invites": {"per_room": {"per_second": 0.5, "burst_count": 3}}}
|
||||
)
|
||||
def test_invites_by_rooms_ratelimit(self):
|
||||
"""Tests that invites in a room are actually rate-limited."""
|
||||
room_id = self.helper.create_room_as(self.user_id)
|
||||
|
||||
for i in range(3):
|
||||
self.helper.invite(room_id, self.user_id, "@user-%s:red" % (i,))
|
||||
|
||||
self.helper.invite(room_id, self.user_id, "@user-4:red", expect_code=429)
|
||||
|
||||
@unittest.override_config(
|
||||
{"rc_invites": {"per_user": {"per_second": 0.5, "burst_count": 3}}}
|
||||
)
|
||||
def test_invites_by_users_ratelimit(self):
|
||||
"""Tests that invites to a specific user are actually rate-limited."""
|
||||
|
||||
for i in range(3):
|
||||
room_id = self.helper.create_room_as(self.user_id)
|
||||
self.helper.invite(room_id, self.user_id, "@other-users:red")
|
||||
|
||||
room_id = self.helper.create_room_as(self.user_id)
|
||||
self.helper.invite(room_id, self.user_id, "@other-users:red", expect_code=429)
|
||||
|
||||
|
||||
class RoomJoinRatelimitTestCase(RoomBase):
|
||||
user_id = "@sid1:red"
|
||||
|
||||
|
|
Loading…
Reference in a new issue