Ratelimit invites by room and target user (#9258)
parent
e19396d622
commit
f2c1560eca
|
@ -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
|
# "remote" for when users are trying to join rooms not on the server (which
|
||||||
# can be more expensive)
|
# can be more expensive)
|
||||||
# - one for ratelimiting how often a user or IP can attempt to validate a 3PID.
|
# - 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.
|
# The defaults are as shown below.
|
||||||
#
|
#
|
||||||
|
@ -862,6 +864,14 @@ log_config: "CONFDIR/SERVERNAME.log.config"
|
||||||
#rc_3pid_validation:
|
#rc_3pid_validation:
|
||||||
# per_second: 0.003
|
# per_second: 0.003
|
||||||
# burst_count: 5
|
# 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
|
# Ratelimiting settings for incoming federation
|
||||||
#
|
#
|
||||||
|
|
|
@ -107,6 +107,15 @@ class RatelimitConfig(Config):
|
||||||
defaults={"per_second": 0.003, "burst_count": 5},
|
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):
|
def generate_config_section(self, **kwargs):
|
||||||
return """\
|
return """\
|
||||||
## Ratelimiting ##
|
## Ratelimiting ##
|
||||||
|
@ -137,6 +146,8 @@ class RatelimitConfig(Config):
|
||||||
# "remote" for when users are trying to join rooms not on the server (which
|
# "remote" for when users are trying to join rooms not on the server (which
|
||||||
# can be more expensive)
|
# can be more expensive)
|
||||||
# - one for ratelimiting how often a user or IP can attempt to validate a 3PID.
|
# - 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.
|
# The defaults are as shown below.
|
||||||
#
|
#
|
||||||
|
@ -174,6 +185,14 @@ class RatelimitConfig(Config):
|
||||||
#rc_3pid_validation:
|
#rc_3pid_validation:
|
||||||
# per_second: 0.003
|
# per_second: 0.003
|
||||||
# burst_count: 5
|
# 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
|
# Ratelimiting settings for incoming federation
|
||||||
#
|
#
|
||||||
|
|
|
@ -810,7 +810,7 @@ class FederationClient(FederationBase):
|
||||||
"User's homeserver does not support this room version",
|
"User's homeserver does not support this room version",
|
||||||
Codes.UNSUPPORTED_ROOM_VERSION,
|
Codes.UNSUPPORTED_ROOM_VERSION,
|
||||||
)
|
)
|
||||||
elif e.code == 403:
|
elif e.code in (403, 429):
|
||||||
raise e.to_synapse_error()
|
raise e.to_synapse_error()
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
|
|
|
@ -1617,6 +1617,10 @@ class FederationHandler(BaseHandler):
|
||||||
if event.state_key == self._server_notices_mxid:
|
if event.state_key == self._server_notices_mxid:
|
||||||
raise SynapseError(HTTPStatus.FORBIDDEN, "Cannot invite this user")
|
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.
|
# 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
|
# (this may get overwritten if we later get a different room version in a
|
||||||
# join dance).
|
# join dance).
|
||||||
|
|
|
@ -126,6 +126,10 @@ class RoomCreationHandler(BaseHandler):
|
||||||
|
|
||||||
self.third_party_event_rules = hs.get_third_party_event_rules()
|
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(
|
async def upgrade_room(
|
||||||
self, requester: Requester, old_room_id: str, new_version: RoomVersion
|
self, requester: Requester, old_room_id: str, new_version: RoomVersion
|
||||||
) -> str:
|
) -> str:
|
||||||
|
@ -662,6 +666,9 @@ class RoomCreationHandler(BaseHandler):
|
||||||
invite_3pid_list = []
|
invite_3pid_list = []
|
||||||
invite_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)
|
await self.event_creation_handler.assert_accepted_privacy_policy(requester)
|
||||||
|
|
||||||
power_level_content_override = config.get("power_level_content_override")
|
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,
|
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
|
# This is only used to get at ratelimit function, and
|
||||||
# maybe_kick_guest_users. It's fine there are multiple of these as
|
# maybe_kick_guest_users. It's fine there are multiple of these as
|
||||||
# it doesn't store state.
|
# it doesn't store state.
|
||||||
|
@ -144,6 +155,12 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
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(
|
async def _local_membership_update(
|
||||||
self,
|
self,
|
||||||
requester: Requester,
|
requester: Requester,
|
||||||
|
@ -387,8 +404,12 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
|
||||||
raise SynapseError(403, "This room has been blocked on this server")
|
raise SynapseError(403, "This room has been blocked on this server")
|
||||||
|
|
||||||
if effective_membership_state == Membership.INVITE:
|
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
|
# 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")
|
raise SynapseError(HTTPStatus.FORBIDDEN, "Cannot invite this user")
|
||||||
|
|
||||||
block_invite = False
|
block_invite = False
|
||||||
|
@ -412,7 +433,7 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
|
||||||
block_invite = True
|
block_invite = True
|
||||||
|
|
||||||
if not await self.spam_checker.user_may_invite(
|
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")
|
logger.info("Blocking invite due to spam checker")
|
||||||
block_invite = True
|
block_invite = True
|
||||||
|
|
|
@ -16,7 +16,7 @@ import logging
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
|
||||||
from synapse.api.constants import EventTypes
|
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.api.room_versions import RoomVersions
|
||||||
from synapse.events import EventBase
|
from synapse.events import EventBase
|
||||||
from synapse.federation.federation_base import event_from_pdu_json
|
from synapse.federation.federation_base import event_from_pdu_json
|
||||||
|
@ -191,6 +191,97 @@ class FederationTestCase(unittest.HomeserverTestCase):
|
||||||
|
|
||||||
self.assertEqual(sg, sg2)
|
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):
|
def _build_and_send_join_event(self, other_server, other_user, room_id):
|
||||||
join_event = self.get_success(
|
join_event = self.get_success(
|
||||||
self.handler.on_make_join_request(other_server, room_id, other_user)
|
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)
|
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):
|
class RoomJoinRatelimitTestCase(RoomBase):
|
||||||
user_id = "@sid1:red"
|
user_id = "@sid1:red"
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue