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

Validate alt_aliases property of canonical alias events #6971

Merged
merged 14 commits into from
Mar 3, 2020
Merged
1 change: 1 addition & 0 deletions changelog.d/6971.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Validate the alt_aliases property of canonical alias events.
1 change: 1 addition & 0 deletions synapse/api/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ class Codes(object):
EXPIRED_ACCOUNT = "ORG_MATRIX_EXPIRED_ACCOUNT"
INVALID_SIGNATURE = "M_INVALID_SIGNATURE"
USER_DEACTIVATED = "M_USER_DEACTIVATED"
BAD_ALIAS = "M_BAD_ALIAS"


class CodeMessageException(RuntimeError):
Expand Down
14 changes: 7 additions & 7 deletions synapse/handlers/directory.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.


import collections
import logging
import string
from typing import List
Expand Down Expand Up @@ -307,15 +305,17 @@ def _update_canonical_alias(self, requester, user_id, room_id, room_alias):
send_update = True
content.pop("alias", "")

# Filter alt_aliases for the removed alias.
alt_aliases = content.pop("alt_aliases", None)
richvdh marked this conversation as resolved.
Show resolved Hide resolved
# If the aliases are not a list (or not found) do not attempt to modify
# the list.
if isinstance(alt_aliases, collections.Sequence):
# Filter the alt_aliases property for the removed alias. Note that the
# value is not modified if alt_aliases is of an unexpected form.
alt_aliases = content.get("alt_aliases")
if isinstance(alt_aliases, (list, tuple)) and alias_str in alt_aliases:
send_update = True
alt_aliases = [alias for alias in alt_aliases if alias != alias_str]

if alt_aliases:
content["alt_aliases"] = alt_aliases
else:
del content["alt_aliases"]

if send_update:
yield self.event_creation_handler.create_and_send_nonmember_event(
Expand Down
47 changes: 44 additions & 3 deletions synapse/handlers/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -888,19 +888,60 @@ def persist_and_notify_client_event(
yield self.base_handler.maybe_kick_guest_users(event, context)

if event.type == EventTypes.CanonicalAlias:
# Check the alias is acually valid (at this time at least)
# Validate a newly added alias or newly added alt_aliases.

original_alias = None
original_alt_aliases = set()

original_event_id = event.unsigned.get("replaces_state")
if original_event_id:
original_event = yield self.store.get_event(original_event_id)

if original_event:
original_alias = original_event.content.get("alias", None)
original_alt_aliases = original_event.content.get("alt_aliases", [])

# Check the alias is currently valid (if it has changed).
room_alias_str = event.content.get("alias", None)
if room_alias_str:
directory_handler = self.hs.get_handlers().directory_handler
if room_alias_str and room_alias_str != original_alias:
room_alias = RoomAlias.from_string(room_alias_str)
directory_handler = self.hs.get_handlers().directory_handler
mapping = yield directory_handler.get_association(room_alias)

if mapping["room_id"] != event.room_id:
raise SynapseError(
400,
"Room alias %s does not point to the room" % (room_alias_str,),
Codes.BAD_ALIAS,
)

# Check that alt_aliases is the proper form.
alt_aliases = event.content.get("alt_aliases", [])
if not isinstance(alt_aliases, (list, tuple)):
raise SynapseError(
400, "The alt_aliases property must be a list.", Codes.INVALID_PARAM
)

# If the old version of alt_aliases is of an unknown form,
# completely replace it.
if not isinstance(original_alt_aliases, (list, tuple)):
original_alt_aliases = []

# Check that each alias is currently valid.
new_alt_aliases = set(alt_aliases) - set(original_alt_aliases)
if new_alt_aliases:
for alias_str in new_alt_aliases:
room_alias = RoomAlias.from_string(alias_str)
mapping = yield directory_handler.get_association(room_alias)

if mapping["room_id"] != event.room_id:
raise SynapseError(
400,
"Room alias %s does not point to the room"
% (room_alias_str,),
Codes.BAD_ALIAS,
)

federation_handler = self.hs.get_handlers().federation_handler

if event.type == EventTypes.Member:
Expand Down
15 changes: 10 additions & 5 deletions synapse/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from signedjson.key import decode_verify_key_bytes
from unpaddedbase64 import decode_base64

from synapse.api.errors import SynapseError
from synapse.api.errors import Codes, SynapseError

# define a version of typing.Collection that works on python 3.5
if sys.version_info[:3] >= (3, 6, 0):
Expand Down Expand Up @@ -166,11 +166,13 @@ def __deepcopy__(self, memo):
return self

@classmethod
def from_string(cls, s):
def from_string(cls, s: str):
"""Parse the string given by 's' into a structure object."""
if len(s) < 1 or s[0:1] != cls.SIGIL:
raise SynapseError(
400, "Expected %s string to start with '%s'" % (cls.__name__, cls.SIGIL)
400,
"Expected %s string to start with '%s'" % (cls.__name__, cls.SIGIL),
Codes.INVALID_PARAM,
)

parts = s[1:].split(":", 1)
Expand All @@ -179,6 +181,7 @@ def from_string(cls, s):
400,
"Expected %s of the form '%slocalname:domain'"
% (cls.__name__, cls.SIGIL),
Codes.INVALID_PARAM,
)

domain = parts[1]
Expand Down Expand Up @@ -235,11 +238,13 @@ class GroupID(DomainSpecificString):
def from_string(cls, s):
group_id = super(GroupID, cls).from_string(s)
if not group_id.localpart:
raise SynapseError(400, "Group ID cannot be empty")
raise SynapseError(400, "Group ID cannot be empty", Codes.INVALID_PARAM)

if contains_invalid_mxid_characters(group_id.localpart):
raise SynapseError(
400, "Group ID can only contain characters a-z, 0-9, or '=_-./'"
400,
"Group ID can only contain characters a-z, 0-9, or '=_-./'",
Codes.INVALID_PARAM,
)

return group_id
Expand Down
66 changes: 30 additions & 36 deletions tests/handlers/test_directory.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ def test_get_remote_association(self):
)

def test_delete_alias_not_allowed(self):
"""Removing an alias should be denied if a user does not have the proper permissions."""
room_id = "!8765qwer:test"
self.get_success(
self.store.create_room_alias_association(self.my_room, room_id, ["test"])
Expand All @@ -101,6 +102,7 @@ def test_delete_alias_not_allowed(self):
)

def test_delete_alias(self):
"""Removing an alias should work when a user does has the proper permissions."""
room_id = "!8765qwer:test"
user_id = "@user:test"
self.get_success(
Expand Down Expand Up @@ -159,30 +161,42 @@ def prepare(self, reactor, clock, hs):
)

self.test_alias = "#test:test"
self.room_alias = RoomAlias.from_string(self.test_alias)
self.room_alias = self._add_alias(self.test_alias)

def _add_alias(self, alias: str) -> RoomAlias:
"""Add an alias to the test room."""
room_alias = RoomAlias.from_string(alias)

# Create a new alias to this room.
self.get_success(
self.store.create_room_alias_association(
self.room_alias, self.room_id, ["test"], self.admin_user
room_alias, self.room_id, ["test"], self.admin_user
)
)
return room_alias

def test_remove_alias(self):
"""Removing an alias that is the canonical alias should remove it there too."""
# Set this new alias as the canonical alias for this room
def _set_canonical_alias(self, content):
"""Configure the canonical alias state on the room."""
self.helper.send_state(
self.room_id,
"m.room.canonical_alias",
{"alias": self.test_alias, "alt_aliases": [self.test_alias]},
tok=self.admin_user_tok,
self.room_id, "m.room.canonical_alias", content, tok=self.admin_user_tok,
)

data = self.get_success(
def _get_canonical_alias(self):
"""Get the canonical alias state of the room."""
return self.get_success(
self.state_handler.get_current_state(
self.room_id, EventTypes.CanonicalAlias, ""
)
)

def test_remove_alias(self):
"""Removing an alias that is the canonical alias should remove it there too."""
# Set this new alias as the canonical alias for this room
self._set_canonical_alias(
{"alias": self.test_alias, "alt_aliases": [self.test_alias]}
)

data = self._get_canonical_alias()
self.assertEqual(data["content"]["alias"], self.test_alias)
self.assertEqual(data["content"]["alt_aliases"], [self.test_alias])

Expand All @@ -193,41 +207,25 @@ def test_remove_alias(self):
)
)

data = self.get_success(
self.state_handler.get_current_state(
self.room_id, EventTypes.CanonicalAlias, ""
)
)
data = self._get_canonical_alias()
self.assertNotIn("alias", data["content"])
self.assertNotIn("alt_aliases", data["content"])

def test_remove_other_alias(self):
"""Removing an alias listed as in alt_aliases should remove it there too."""
# Create a second alias.
other_test_alias = "#test2:test"
other_room_alias = RoomAlias.from_string(other_test_alias)
self.get_success(
self.store.create_room_alias_association(
other_room_alias, self.room_id, ["test"], self.admin_user
)
)
other_room_alias = self._add_alias(other_test_alias)

# Set the alias as the canonical alias for this room.
self.helper.send_state(
self.room_id,
"m.room.canonical_alias",
self._set_canonical_alias(
{
"alias": self.test_alias,
"alt_aliases": [self.test_alias, other_test_alias],
},
tok=self.admin_user_tok,
}
)

data = self.get_success(
self.state_handler.get_current_state(
self.room_id, EventTypes.CanonicalAlias, ""
)
)
data = self._get_canonical_alias()
self.assertEqual(data["content"]["alias"], self.test_alias)
self.assertEqual(
data["content"]["alt_aliases"], [self.test_alias, other_test_alias]
Expand All @@ -240,11 +238,7 @@ def test_remove_other_alias(self):
)
)

data = self.get_success(
self.state_handler.get_current_state(
self.room_id, EventTypes.CanonicalAlias, ""
)
)
data = self._get_canonical_alias()
self.assertEqual(data["content"]["alias"], self.test_alias)
self.assertEqual(data["content"]["alt_aliases"], [self.test_alias])

Expand Down
Loading