Merge branch 'master' into develop

This commit is contained in:
Richard van der Hoff 2021-08-31 14:09:48 +01:00
commit 5d9e7e0c71
8 changed files with 214 additions and 7 deletions

View file

@ -1,5 +1,36 @@
Users will stop receiving message updates via email for addresses that were previously linked to their account Users will stop receiving message updates via email for addresses that were previously linked to their account
Synapse 1.41.1 (2021-08-31)
===========================
Due to the two security issues highlighted below, server administrators are encouraged to update Synapse. We are not aware of these vulnerabilities being exploited in the wild.
Security advisory
-----------------
The following issues are fixed in v1.41.1.
- **[GHSA-3x4c-pq33-4w3q](https://github.com/matrix-org/synapse/security/advisories/GHSA-3x4c-pq33-4w3q) / [CVE-2021-39164](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-39164): Enumerating a private room's list of members and their display names.**
If an unauthorized user both knows the Room ID of a private room *and* that room's history visibility is set to `shared`, then they may be able to enumerate the room's members, including their display names.
The unauthorized user must be on the same homeserver as a user who is a member of the target room.
Fixed by [52c7a51cf](https://github.com/matrix-org/synapse/commit/52c7a51cf).
- **[GHSA-jj53-8fmw-f2w2](https://github.com/matrix-org/synapse/security/advisories/GHSA-jj53-8fmw-f2w2) / [CVE-2021-39163](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-39163): Disclosing a private room's name, avatar, topic, and number of members.**
If an unauthorized user knows the Room ID of a private room, then its name, avatar, topic, and number of members may be disclosed through Group / Community features.
The unauthorized user must be on the same homeserver as a user who is a member of the target room, and their homeserver must allow non-administrators to create groups (`enable_group_creation` in the Synapse configuration; off by default).
Fixed by [cb35df940a](https://github.com/matrix-org/synapse/commit/cb35df940a), [\#10723](https://github.com/matrix-org/synapse/issues/10723).
Bugfixes
--------
- Fix a regression introduced in Synapse 1.41 which broke email transmission on systems using older versions of the Twisted library. ([\#10713](https://github.com/matrix-org/synapse/issues/10713))
Synapse 1.41.0 (2021-08-24) Synapse 1.41.0 (2021-08-24)
=========================== ===========================

1
changelog.d/10723.bugfix Normal file
View file

@ -0,0 +1 @@
Fix unauthorised exposure of room metadata to communities.

6
debian/changelog vendored
View file

@ -1,3 +1,9 @@
matrix-synapse-py3 (1.41.1) stable; urgency=high
* New synapse release 1.41.1.
-- Synapse Packaging team <packages@matrix.org> Tue, 31 Aug 2021 12:59:10 +0100
matrix-synapse-py3 (1.41.0) stable; urgency=medium matrix-synapse-py3 (1.41.0) stable; urgency=medium
* New synapse release 1.41.0. * New synapse release 1.41.0.

View file

@ -47,7 +47,7 @@ try:
except ImportError: except ImportError:
pass pass
__version__ = "1.41.0" __version__ = "1.41.1"
if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)):
# We import here so that we don't have to install a bunch of deps when # We import here so that we don't have to install a bunch of deps when

View file

@ -332,6 +332,13 @@ class GroupsServerWorkerHandler:
requester_user_id, group_id requester_user_id, group_id
) )
# Note! room_results["is_public"] is about whether the room is considered
# public from the group's point of view. (i.e. whether non-group members
# should be able to see the room is in the group).
# This is not the same as whether the room itself is public (in the sense
# of being visible in the room directory).
# As such, room_results["is_public"] itself is not sufficient to determine
# whether any given user is permitted to see the room's metadata.
room_results = await self.store.get_rooms_in_group( room_results = await self.store.get_rooms_in_group(
group_id, include_private=is_user_in_group group_id, include_private=is_user_in_group
) )
@ -341,8 +348,15 @@ class GroupsServerWorkerHandler:
room_id = room_result["room_id"] room_id = room_result["room_id"]
joined_users = await self.store.get_users_in_room(room_id) joined_users = await self.store.get_users_in_room(room_id)
# check the user is actually allowed to see the room before showing it to them
allow_private = requester_user_id in joined_users
entry = await self.room_list_handler.generate_room_entry( entry = await self.room_list_handler.generate_room_entry(
room_id, len(joined_users), with_alias=False, allow_private=True room_id,
len(joined_users),
with_alias=False,
allow_private=allow_private,
) )
if not entry: if not entry:
@ -354,7 +368,7 @@ class GroupsServerWorkerHandler:
chunk.sort(key=lambda e: -e["num_joined_members"]) chunk.sort(key=lambda e: -e["num_joined_members"])
return {"chunk": chunk, "total_room_count_estimate": len(room_results)} return {"chunk": chunk, "total_room_count_estimate": len(chunk)}
class GroupsServerHandler(GroupsServerWorkerHandler): class GroupsServerHandler(GroupsServerWorkerHandler):

View file

@ -183,20 +183,37 @@ class MessageHandler:
if not last_events: if not last_events:
raise NotFoundError("Can't find event for token %s" % (at_token,)) raise NotFoundError("Can't find event for token %s" % (at_token,))
last_event = last_events[0]
# check whether the user is in the room at that time to determine
# whether they should be treated as peeking.
state_map = await self.state_store.get_state_for_event(
last_event.event_id,
StateFilter.from_types([(EventTypes.Member, user_id)]),
)
joined = False
membership_event = state_map.get((EventTypes.Member, user_id))
if membership_event:
joined = membership_event.membership == Membership.JOIN
is_peeking = not joined
visible_events = await filter_events_for_client( visible_events = await filter_events_for_client(
self.storage, self.storage,
user_id, user_id,
last_events, last_events,
filter_send_to_client=False, filter_send_to_client=False,
is_peeking=is_peeking,
) )
event = last_events[0]
if visible_events: if visible_events:
room_state_events = await self.state_store.get_state_for_events( room_state_events = await self.state_store.get_state_for_events(
[event.event_id], state_filter=state_filter [last_event.event_id], state_filter=state_filter
) )
room_state: Mapping[Any, EventBase] = room_state_events[event.event_id] room_state: Mapping[Any, EventBase] = room_state_events[
last_event.event_id
]
else: else:
raise AuthError( raise AuthError(
403, 403,

View file

@ -0,0 +1,56 @@
# Copyright 2021 The Matrix.org Foundation C.I.C.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from synapse.rest.client import groups, room
from tests import unittest
from tests.unittest import override_config
class GroupsTestCase(unittest.HomeserverTestCase):
user_id = "@alice:test"
room_creator_user_id = "@bob:test"
servlets = [room.register_servlets, groups.register_servlets]
@override_config({"enable_group_creation": True})
def test_rooms_limited_by_visibility(self):
group_id = "+spqr:test"
# Alice creates a group
channel = self.make_request("POST", "/create_group", {"localpart": "spqr"})
self.assertEquals(channel.code, 200, msg=channel.text_body)
self.assertEquals(channel.json_body, {"group_id": group_id})
# Bob creates a private room
room_id = self.helper.create_room_as(self.room_creator_user_id, is_public=False)
self.helper.auth_user_id = self.room_creator_user_id
self.helper.send_state(
room_id, "m.room.name", {"name": "bob's secret room"}, tok=None
)
self.helper.auth_user_id = self.user_id
# Alice adds the room to her group.
channel = self.make_request(
"PUT", f"/groups/{group_id}/admin/rooms/{room_id}", {}
)
self.assertEquals(channel.code, 200, msg=channel.text_body)
self.assertEquals(channel.json_body, {})
# Alice now tries to retrieve the room list of the space.
channel = self.make_request("GET", f"/groups/{group_id}/rooms")
self.assertEquals(channel.code, 200, msg=channel.text_body)
self.assertEquals(
channel.json_body, {"chunk": [], "total_room_count_estimate": 0}
)

View file

@ -29,7 +29,7 @@ from synapse.api.constants import EventContentFields, EventTypes, Membership
from synapse.api.errors import HttpResponseException from synapse.api.errors import HttpResponseException
from synapse.handlers.pagination import PurgeStatus from synapse.handlers.pagination import PurgeStatus
from synapse.rest import admin from synapse.rest import admin
from synapse.rest.client import account, directory, login, profile, room from synapse.rest.client import account, directory, login, profile, room, sync
from synapse.types import JsonDict, RoomAlias, UserID, create_requester from synapse.types import JsonDict, RoomAlias, UserID, create_requester
from synapse.util.stringutils import random_string from synapse.util.stringutils import random_string
@ -381,6 +381,8 @@ class RoomPermissionsTestCase(RoomBase):
class RoomsMemberListTestCase(RoomBase): class RoomsMemberListTestCase(RoomBase):
"""Tests /rooms/$room_id/members/list REST events.""" """Tests /rooms/$room_id/members/list REST events."""
servlets = RoomBase.servlets + [sync.register_servlets]
user_id = "@sid1:red" user_id = "@sid1:red"
def test_get_member_list(self): def test_get_member_list(self):
@ -397,6 +399,86 @@ class RoomsMemberListTestCase(RoomBase):
channel = self.make_request("GET", "/rooms/%s/members" % room_id) channel = self.make_request("GET", "/rooms/%s/members" % room_id)
self.assertEquals(403, channel.code, msg=channel.result["body"]) self.assertEquals(403, channel.code, msg=channel.result["body"])
def test_get_member_list_no_permission_with_at_token(self):
"""
Tests that a stranger to the room cannot get the member list
(in the case that they use an at token).
"""
room_id = self.helper.create_room_as("@someone.else:red")
# first sync to get an at token
channel = self.make_request("GET", "/sync")
self.assertEquals(200, channel.code)
sync_token = channel.json_body["next_batch"]
# check that permission is denied for @sid1:red to get the
# memberships of @someone.else:red's room.
channel = self.make_request(
"GET",
f"/rooms/{room_id}/members?at={sync_token}",
)
self.assertEquals(403, channel.code, msg=channel.result["body"])
def test_get_member_list_no_permission_former_member(self):
"""
Tests that a former member of the room can not get the member list.
"""
# create a room, invite the user and the user joins
room_id = self.helper.create_room_as("@alice:red")
self.helper.invite(room_id, "@alice:red", self.user_id)
self.helper.join(room_id, self.user_id)
# check that the user can see the member list to start with
channel = self.make_request("GET", "/rooms/%s/members" % room_id)
self.assertEquals(200, channel.code, msg=channel.result["body"])
# ban the user
self.helper.change_membership(room_id, "@alice:red", self.user_id, "ban")
# check the user can no longer see the member list
channel = self.make_request("GET", "/rooms/%s/members" % room_id)
self.assertEquals(403, channel.code, msg=channel.result["body"])
def test_get_member_list_no_permission_former_member_with_at_token(self):
"""
Tests that a former member of the room can not get the member list
(in the case that they use an at token).
"""
# create a room, invite the user and the user joins
room_id = self.helper.create_room_as("@alice:red")
self.helper.invite(room_id, "@alice:red", self.user_id)
self.helper.join(room_id, self.user_id)
# sync to get an at token
channel = self.make_request("GET", "/sync")
self.assertEquals(200, channel.code)
sync_token = channel.json_body["next_batch"]
# check that the user can see the member list to start with
channel = self.make_request(
"GET", "/rooms/%s/members?at=%s" % (room_id, sync_token)
)
self.assertEquals(200, channel.code, msg=channel.result["body"])
# ban the user (Note: the user is actually allowed to see this event and
# state so that they know they're banned!)
self.helper.change_membership(room_id, "@alice:red", self.user_id, "ban")
# invite a third user and let them join
self.helper.invite(room_id, "@alice:red", "@bob:red")
self.helper.join(room_id, "@bob:red")
# now, with the original user, sync again to get a new at token
channel = self.make_request("GET", "/sync")
self.assertEquals(200, channel.code)
sync_token = channel.json_body["next_batch"]
# check the user can no longer see the updated member list
channel = self.make_request(
"GET", "/rooms/%s/members?at=%s" % (room_id, sync_token)
)
self.assertEquals(403, channel.code, msg=channel.result["body"])
def test_get_member_list_mixed_memberships(self): def test_get_member_list_mixed_memberships(self):
room_creator = "@some_other_guy:red" room_creator = "@some_other_guy:red"
room_id = self.helper.create_room_as(room_creator) room_id = self.helper.create_room_as(room_creator)