Denial of service (via resource exhaustion) due to improper input validation on groups/communities endpoints
Description
Synapse is a Matrix reference homeserver written in python (pypi package matrix-synapse). Matrix is an ecosystem for open federated Instant Messaging and VoIP. In Synapse before version 1.28.0 Synapse is missing input validation of some parameters on the endpoints used to confirm third-party identifiers could cause excessive use of disk space and memory leading to resource exhaustion. Note that the groups feature is not part of the Matrix specification and the chosen maximum lengths are arbitrary. Not all clients might abide by them. Refer to referenced GitHub security advisory for additional details including workarounds.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Synapse before 1.28.0 lacks input validation on endpoints for confirming third-party identifiers, allowing resource exhaustion via disk and memory consumption.
Vulnerability
In Synapse (matrix-synapse) before version 1.28.0, the endpoints used to confirm third-party identifiers lack input validation on certain parameters [3]. This allows an attacker to supply excessively long token, sid, client_secret, or next_link parameters, leading to excessive disk space and memory usage [1][2][3]. The groups feature, which is not part of the Matrix specification, is also affected by arbitrary maximum lengths that not all clients may enforce [3].
Exploitation
An attacker can send crafted HTTP requests to the vulnerable endpoints with long parameter values. The exploitation requires network access to the Synapse server but no authentication or user interaction, as these endpoints are typically publicly reachable to process third-party identifier confirmation flows [3]. By sending several such requests, the attacker forces the server to store and process oversized data, consuming memory and disk space [1][2][3].
Impact
Successful exploitation leads to resource exhaustion of the Synapse server. The attacker can cause excessive use of disk space and memory, potentially crashing the server or making it unavailable for legitimate users. This is a denial-of-service (DoS) condition that affects the availability of the Matrix homeserver [3].
Mitigation
Synapse version 1.28.0 fixes the vulnerability by adding input validation and type hints to the affected endpoints [1][2][3]. Administrators should upgrade to 1.28.0 or later. If upgrading is not immediately possible, the advisory recommends disabling the third-party identifier confirmation endpoints temporarily or reviewing the threepid configuration [3].
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
matrix-synapsePyPI | < 1.28.0 | 1.28.0 |
Affected products
2- matrix-org/synapsev5Range: < 1.28.0
Patches
2d2f0ec12d5c8Add type hints to groups code. (#9393)
9 files changed · +341 −124
changelog.d/9321.bugfix+1 −1 modified@@ -1 +1 @@ -Assert a maximum length for the `client_secret` parameter for spec compliance. +Assert a maximum length for some parameters for spec compliance.
changelog.d/9393.bugfix+1 −0 added@@ -0,0 +1 @@ +Assert a maximum length for some parameters for spec compliance.
mypy.ini+1 −0 modified@@ -23,6 +23,7 @@ files = synapse/events/validator.py, synapse/events/spamcheck.py, synapse/federation, + synapse/groups, synapse/handlers, synapse/http/client.py, synapse/http/federation/matrix_federation_agent.py,
synapse/api/constants.py+5 −0 modified@@ -27,6 +27,11 @@ # the maximum length for a user id is 255 characters MAX_USERID_LENGTH = 255 +# The maximum length for a group id is 255 characters +MAX_GROUPID_LENGTH = 255 +MAX_GROUP_CATEGORYID_LENGTH = 255 +MAX_GROUP_ROLEID_LENGTH = 255 + class Membership:
synapse/federation/transport/server.py+39 −2 modified@@ -21,6 +21,7 @@ from typing import Optional, Tuple, Type import synapse +from synapse.api.constants import MAX_GROUP_CATEGORYID_LENGTH, MAX_GROUP_ROLEID_LENGTH from synapse.api.errors import Codes, FederationDeniedError, SynapseError from synapse.api.room_versions import RoomVersions from synapse.api.urls import ( @@ -1118,7 +1119,17 @@ async def on_POST(self, origin, content, query, group_id, category_id, room_id): raise SynapseError(403, "requester_user_id doesn't match origin") if category_id == "": - raise SynapseError(400, "category_id cannot be empty string") + raise SynapseError( + 400, "category_id cannot be empty string", Codes.INVALID_PARAM + ) + + if len(category_id) > MAX_GROUP_CATEGORYID_LENGTH: + raise SynapseError( + 400, + "category_id may not be longer than %s characters" + % (MAX_GROUP_CATEGORYID_LENGTH,), + Codes.INVALID_PARAM, + ) resp = await self.handler.update_group_summary_room( group_id, @@ -1184,6 +1195,14 @@ async def on_POST(self, origin, content, query, group_id, category_id): if category_id == "": raise SynapseError(400, "category_id cannot be empty string") + if len(category_id) > MAX_GROUP_CATEGORYID_LENGTH: + raise SynapseError( + 400, + "category_id may not be longer than %s characters" + % (MAX_GROUP_CATEGORYID_LENGTH,), + Codes.INVALID_PARAM, + ) + resp = await self.handler.upsert_group_category( group_id, requester_user_id, category_id, content ) @@ -1240,7 +1259,17 @@ async def on_POST(self, origin, content, query, group_id, role_id): raise SynapseError(403, "requester_user_id doesn't match origin") if role_id == "": - raise SynapseError(400, "role_id cannot be empty string") + raise SynapseError( + 400, "role_id cannot be empty string", Codes.INVALID_PARAM + ) + + if len(role_id) > MAX_GROUP_ROLEID_LENGTH: + raise SynapseError( + 400, + "role_id may not be longer than %s characters" + % (MAX_GROUP_ROLEID_LENGTH,), + Codes.INVALID_PARAM, + ) resp = await self.handler.update_group_role( group_id, requester_user_id, role_id, content @@ -1285,6 +1314,14 @@ async def on_POST(self, origin, content, query, group_id, role_id, user_id): if role_id == "": raise SynapseError(400, "role_id cannot be empty string") + if len(role_id) > MAX_GROUP_ROLEID_LENGTH: + raise SynapseError( + 400, + "role_id may not be longer than %s characters" + % (MAX_GROUP_ROLEID_LENGTH,), + Codes.INVALID_PARAM, + ) + resp = await self.handler.update_group_summary_user( group_id, requester_user_id,
synapse/groups/attestations.py+24 −13 modified@@ -37,13 +37,16 @@ import logging import random -from typing import Tuple +from typing import TYPE_CHECKING, Optional, Tuple from signedjson.sign import sign_json from synapse.api.errors import HttpResponseException, RequestSendFailed, SynapseError from synapse.metrics.background_process_metrics import run_as_background_process -from synapse.types import get_domain_from_id +from synapse.types import JsonDict, get_domain_from_id + +if TYPE_CHECKING: + from synapse.app.homeserver import HomeServer logger = logging.getLogger(__name__) @@ -63,15 +66,19 @@ class GroupAttestationSigning: """Creates and verifies group attestations.""" - def __init__(self, hs): + def __init__(self, hs: "HomeServer"): self.keyring = hs.get_keyring() self.clock = hs.get_clock() self.server_name = hs.hostname self.signing_key = hs.signing_key async def verify_attestation( - self, attestation, group_id, user_id, server_name=None - ): + self, + attestation: JsonDict, + group_id: str, + user_id: str, + server_name: Optional[str] = None, + ) -> None: """Verifies that the given attestation matches the given parameters. An optional server_name can be supplied to explicitly set which server's @@ -100,16 +107,18 @@ async def verify_attestation( if valid_until_ms < now: raise SynapseError(400, "Attestation expired") + assert server_name is not None await self.keyring.verify_json_for_server( server_name, attestation, now, "Group attestation" ) - def create_attestation(self, group_id, user_id): + def create_attestation(self, group_id: str, user_id: str) -> JsonDict: """Create an attestation for the group_id and user_id with default validity length. """ - validity_period = DEFAULT_ATTESTATION_LENGTH_MS - validity_period *= random.uniform(*DEFAULT_ATTESTATION_JITTER) + validity_period = DEFAULT_ATTESTATION_LENGTH_MS * random.uniform( + *DEFAULT_ATTESTATION_JITTER + ) valid_until_ms = int(self.clock.time_msec() + validity_period) return sign_json( @@ -126,7 +135,7 @@ def create_attestation(self, group_id, user_id): class GroupAttestionRenewer: """Responsible for sending and receiving attestation updates.""" - def __init__(self, hs): + def __init__(self, hs: "HomeServer"): self.clock = hs.get_clock() self.store = hs.get_datastore() self.assestations = hs.get_groups_attestation_signing() @@ -139,7 +148,9 @@ def __init__(self, hs): self._start_renew_attestations, 30 * 60 * 1000 ) - async def on_renew_attestation(self, group_id, user_id, content): + async def on_renew_attestation( + self, group_id: str, user_id: str, content: JsonDict + ) -> JsonDict: """When a remote updates an attestation""" attestation = content["attestation"] @@ -154,10 +165,10 @@ async def on_renew_attestation(self, group_id, user_id, content): return {} - def _start_renew_attestations(self): + def _start_renew_attestations(self) -> None: return run_as_background_process("renew_attestations", self._renew_attestations) - async def _renew_attestations(self): + async def _renew_attestations(self) -> None: """Called periodically to check if we need to update any of our attestations""" now = self.clock.time_msec() @@ -166,7 +177,7 @@ async def _renew_attestations(self): now + UPDATE_ATTESTATION_TIME_MS ) - async def _renew_attestation(group_user: Tuple[str, str]): + async def _renew_attestation(group_user: Tuple[str, str]) -> None: group_id, user_id = group_user try: if not self.is_mine_id(group_id):
synapse/groups/groups_server.py+143 −83 modified@@ -16,12 +16,17 @@ # limitations under the License. import logging +from typing import TYPE_CHECKING, Optional from synapse.api.errors import Codes, SynapseError +from synapse.handlers.groups_local import GroupsLocalHandler from synapse.handlers.profile import MAX_AVATAR_URL_LEN, MAX_DISPLAYNAME_LEN -from synapse.types import GroupID, RoomID, UserID, get_domain_from_id +from synapse.types import GroupID, JsonDict, RoomID, UserID, get_domain_from_id from synapse.util.async_helpers import concurrently_execute +if TYPE_CHECKING: + from synapse.app.homeserver import HomeServer + logger = logging.getLogger(__name__) @@ -39,7 +44,7 @@ class GroupsServerWorkerHandler: - def __init__(self, hs): + def __init__(self, hs: "HomeServer"): self.hs = hs self.store = hs.get_datastore() self.room_list_handler = hs.get_room_list_handler() @@ -54,16 +59,21 @@ def __init__(self, hs): self.profile_handler = hs.get_profile_handler() async def check_group_is_ours( - self, group_id, requester_user_id, and_exists=False, and_is_admin=None - ): + self, + group_id: str, + requester_user_id: str, + and_exists: bool = False, + and_is_admin: Optional[str] = None, + ) -> Optional[dict]: """Check that the group is ours, and optionally if it exists. If group does exist then return group. Args: - group_id (str) - and_exists (bool): whether to also check if group exists - and_is_admin (str): whether to also check if given str is a user_id + group_id: The group ID to check. + requester_user_id: The user ID of the requester. + and_exists: whether to also check if group exists + and_is_admin: whether to also check if given str is a user_id that is an admin """ if not self.is_mine_id(group_id): @@ -86,7 +96,9 @@ async def check_group_is_ours( return group - async def get_group_summary(self, group_id, requester_user_id): + async def get_group_summary( + self, group_id: str, requester_user_id: str + ) -> JsonDict: """Get the summary for a group as seen by requester_user_id. The group summary consists of the profile of the room, and a curated @@ -119,29 +131,31 @@ async def get_group_summary(self, group_id, requester_user_id): entry = await self.room_list_handler.generate_room_entry( room_id, len(joined_users), with_alias=False, allow_private=True ) + if entry is None: + continue entry = dict(entry) # so we don't change what's cached entry.pop("room_id", None) room_entry["profile"] = entry rooms.sort(key=lambda e: e.get("order", 0)) - for entry in users: - user_id = entry["user_id"] + for user in users: + user_id = user["user_id"] if not self.is_mine_id(requester_user_id): attestation = await self.store.get_remote_attestation(group_id, user_id) if not attestation: continue - entry["attestation"] = attestation + user["attestation"] = attestation else: - entry["attestation"] = self.attestations.create_attestation( + user["attestation"] = self.attestations.create_attestation( group_id, user_id ) user_profile = await self.profile_handler.get_profile_from_cache(user_id) - entry.update(user_profile) + user.update(user_profile) users.sort(key=lambda e: e.get("order", 0)) @@ -164,40 +178,43 @@ async def get_group_summary(self, group_id, requester_user_id): "user": membership_info, } - async def get_group_categories(self, group_id, requester_user_id): + async def get_group_categories( + self, group_id: str, requester_user_id: str + ) -> JsonDict: """Get all categories in a group (as seen by user)""" await self.check_group_is_ours(group_id, requester_user_id, and_exists=True) categories = await self.store.get_group_categories(group_id=group_id) return {"categories": categories} - async def get_group_category(self, group_id, requester_user_id, category_id): + async def get_group_category( + self, group_id: str, requester_user_id: str, category_id: str + ) -> JsonDict: """Get a specific category in a group (as seen by user)""" await self.check_group_is_ours(group_id, requester_user_id, and_exists=True) - res = await self.store.get_group_category( + return await self.store.get_group_category( group_id=group_id, category_id=category_id ) - logger.info("group %s", res) - - return res - - async def get_group_roles(self, group_id, requester_user_id): + async def get_group_roles(self, group_id: str, requester_user_id: str) -> JsonDict: """Get all roles in a group (as seen by user)""" await self.check_group_is_ours(group_id, requester_user_id, and_exists=True) roles = await self.store.get_group_roles(group_id=group_id) return {"roles": roles} - async def get_group_role(self, group_id, requester_user_id, role_id): + async def get_group_role( + self, group_id: str, requester_user_id: str, role_id: str + ) -> JsonDict: """Get a specific role in a group (as seen by user)""" await self.check_group_is_ours(group_id, requester_user_id, and_exists=True) - res = await self.store.get_group_role(group_id=group_id, role_id=role_id) - return res + return await self.store.get_group_role(group_id=group_id, role_id=role_id) - async def get_group_profile(self, group_id, requester_user_id): + async def get_group_profile( + self, group_id: str, requester_user_id: str + ) -> JsonDict: """Get the group profile as seen by requester_user_id""" await self.check_group_is_ours(group_id, requester_user_id) @@ -219,7 +236,9 @@ async def get_group_profile(self, group_id, requester_user_id): else: raise SynapseError(404, "Unknown group") - async def get_users_in_group(self, group_id, requester_user_id): + async def get_users_in_group( + self, group_id: str, requester_user_id: str + ) -> JsonDict: """Get the users in group as seen by requester_user_id. The ordering is arbitrary at the moment @@ -268,7 +287,9 @@ async def get_users_in_group(self, group_id, requester_user_id): return {"chunk": chunk, "total_user_count_estimate": len(user_results)} - async def get_invited_users_in_group(self, group_id, requester_user_id): + async def get_invited_users_in_group( + self, group_id: str, requester_user_id: str + ) -> JsonDict: """Get the users that have been invited to a group as seen by requester_user_id. The ordering is arbitrary at the moment @@ -298,7 +319,9 @@ async def get_invited_users_in_group(self, group_id, requester_user_id): return {"chunk": user_profiles, "total_user_count_estimate": len(invited_users)} - async def get_rooms_in_group(self, group_id, requester_user_id): + async def get_rooms_in_group( + self, group_id: str, requester_user_id: str + ) -> JsonDict: """Get the rooms in group as seen by requester_user_id This returns rooms in order of decreasing number of joined users @@ -336,15 +359,20 @@ async def get_rooms_in_group(self, group_id, requester_user_id): class GroupsServerHandler(GroupsServerWorkerHandler): - def __init__(self, hs): + def __init__(self, hs: "HomeServer"): super().__init__(hs) # Ensure attestations get renewed hs.get_groups_attestation_renewer() async def update_group_summary_room( - self, group_id, requester_user_id, room_id, category_id, content - ): + self, + group_id: str, + requester_user_id: str, + room_id: str, + category_id: str, + content: JsonDict, + ) -> JsonDict: """Add/update a room to the group summary""" await self.check_group_is_ours( group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id @@ -367,8 +395,8 @@ async def update_group_summary_room( return {} async def delete_group_summary_room( - self, group_id, requester_user_id, room_id, category_id - ): + self, group_id: str, requester_user_id: str, room_id: str, category_id: str + ) -> JsonDict: """Remove a room from the summary""" await self.check_group_is_ours( group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id @@ -380,7 +408,9 @@ async def delete_group_summary_room( return {} - async def set_group_join_policy(self, group_id, requester_user_id, content): + async def set_group_join_policy( + self, group_id: str, requester_user_id: str, content: JsonDict + ) -> JsonDict: """Sets the group join policy. Currently supported policies are: @@ -400,8 +430,8 @@ async def set_group_join_policy(self, group_id, requester_user_id, content): return {} async def update_group_category( - self, group_id, requester_user_id, category_id, content - ): + self, group_id: str, requester_user_id: str, category_id: str, content: JsonDict + ) -> JsonDict: """Add/Update a group category""" await self.check_group_is_ours( group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id @@ -419,7 +449,9 @@ async def update_group_category( return {} - async def delete_group_category(self, group_id, requester_user_id, category_id): + async def delete_group_category( + self, group_id: str, requester_user_id: str, category_id: str + ) -> JsonDict: """Delete a group category""" await self.check_group_is_ours( group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id @@ -431,7 +463,9 @@ async def delete_group_category(self, group_id, requester_user_id, category_id): return {} - async def update_group_role(self, group_id, requester_user_id, role_id, content): + async def update_group_role( + self, group_id: str, requester_user_id: str, role_id: str, content: JsonDict + ) -> JsonDict: """Add/update a role in a group""" await self.check_group_is_ours( group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id @@ -447,7 +481,9 @@ async def update_group_role(self, group_id, requester_user_id, role_id, content) return {} - async def delete_group_role(self, group_id, requester_user_id, role_id): + async def delete_group_role( + self, group_id: str, requester_user_id: str, role_id: str + ) -> JsonDict: """Remove role from group""" await self.check_group_is_ours( group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id @@ -458,8 +494,13 @@ async def delete_group_role(self, group_id, requester_user_id, role_id): return {} async def update_group_summary_user( - self, group_id, requester_user_id, user_id, role_id, content - ): + self, + group_id: str, + requester_user_id: str, + user_id: str, + role_id: str, + content: JsonDict, + ) -> JsonDict: """Add/update a users entry in the group summary""" await self.check_group_is_ours( group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id @@ -480,8 +521,8 @@ async def update_group_summary_user( return {} async def delete_group_summary_user( - self, group_id, requester_user_id, user_id, role_id - ): + self, group_id: str, requester_user_id: str, user_id: str, role_id: str + ) -> JsonDict: """Remove a user from the group summary""" await self.check_group_is_ours( group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id @@ -493,7 +534,9 @@ async def delete_group_summary_user( return {} - async def update_group_profile(self, group_id, requester_user_id, content): + async def update_group_profile( + self, group_id: str, requester_user_id: str, content: JsonDict + ) -> None: """Update the group profile""" await self.check_group_is_ours( group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id @@ -524,7 +567,9 @@ async def update_group_profile(self, group_id, requester_user_id, content): await self.store.update_group_profile(group_id, profile) - async def add_room_to_group(self, group_id, requester_user_id, room_id, content): + async def add_room_to_group( + self, group_id: str, requester_user_id: str, room_id: str, content: JsonDict + ) -> JsonDict: """Add room to group""" RoomID.from_string(room_id) # Ensure valid room id @@ -539,8 +584,13 @@ async def add_room_to_group(self, group_id, requester_user_id, room_id, content) return {} async def update_room_in_group( - self, group_id, requester_user_id, room_id, config_key, content - ): + self, + group_id: str, + requester_user_id: str, + room_id: str, + config_key: str, + content: JsonDict, + ) -> JsonDict: """Update room in group""" RoomID.from_string(room_id) # Ensure valid room id @@ -559,7 +609,9 @@ async def update_room_in_group( return {} - async def remove_room_from_group(self, group_id, requester_user_id, room_id): + async def remove_room_from_group( + self, group_id: str, requester_user_id: str, room_id: str + ) -> JsonDict: """Remove room from group""" await self.check_group_is_ours( group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id @@ -569,12 +621,16 @@ async def remove_room_from_group(self, group_id, requester_user_id, room_id): return {} - async def invite_to_group(self, group_id, user_id, requester_user_id, content): + async def invite_to_group( + self, group_id: str, user_id: str, requester_user_id: str, content: JsonDict + ) -> JsonDict: """Invite user to group""" group = await self.check_group_is_ours( group_id, requester_user_id, and_exists=True, and_is_admin=requester_user_id ) + if not group: + raise SynapseError(400, "Group does not exist", errcode=Codes.BAD_STATE) # TODO: Check if user knocked @@ -597,6 +653,9 @@ async def invite_to_group(self, group_id, user_id, requester_user_id, content): if self.hs.is_mine_id(user_id): groups_local = self.hs.get_groups_local_handler() + assert isinstance( + groups_local, GroupsLocalHandler + ), "Workers cannot invites users to groups." res = await groups_local.on_invite(group_id, user_id, content) local_attestation = None else: @@ -632,6 +691,7 @@ async def invite_to_group(self, group_id, user_id, requester_user_id, content): local_attestation=local_attestation, remote_attestation=remote_attestation, ) + return {"state": "join"} elif res["state"] == "invite": await self.store.add_group_invite(group_id, user_id) return {"state": "invite"} @@ -640,13 +700,17 @@ async def invite_to_group(self, group_id, user_id, requester_user_id, content): else: raise SynapseError(502, "Unknown state returned by HS") - async def _add_user(self, group_id, user_id, content): + async def _add_user( + self, group_id: str, user_id: str, content: JsonDict + ) -> Optional[JsonDict]: """Add a user to a group based on a content dict. See accept_invite, join_group. """ if not self.hs.is_mine_id(user_id): - local_attestation = self.attestations.create_attestation(group_id, user_id) + local_attestation = self.attestations.create_attestation( + group_id, user_id + ) # type: Optional[JsonDict] remote_attestation = content["attestation"] @@ -670,7 +734,9 @@ async def _add_user(self, group_id, user_id, content): return local_attestation - async def accept_invite(self, group_id, requester_user_id, content): + async def accept_invite( + self, group_id: str, requester_user_id: str, content: JsonDict + ) -> JsonDict: """User tries to accept an invite to the group. This is different from them asking to join, and so should error if no @@ -689,7 +755,9 @@ async def accept_invite(self, group_id, requester_user_id, content): return {"state": "join", "attestation": local_attestation} - async def join_group(self, group_id, requester_user_id, content): + async def join_group( + self, group_id: str, requester_user_id: str, content: JsonDict + ) -> JsonDict: """User tries to join the group. This will error if the group requires an invite/knock to join @@ -698,32 +766,18 @@ async def join_group(self, group_id, requester_user_id, content): group_info = await self.check_group_is_ours( group_id, requester_user_id, and_exists=True ) + if not group_info: + raise SynapseError(404, "Group does not exist", errcode=Codes.NOT_FOUND) if group_info["join_policy"] != "open": raise SynapseError(403, "Group is not publicly joinable") local_attestation = await self._add_user(group_id, requester_user_id, content) return {"state": "join", "attestation": local_attestation} - async def knock(self, group_id, requester_user_id, content): - """A user requests becoming a member of the group""" - await self.check_group_is_ours(group_id, requester_user_id, and_exists=True) - - raise NotImplementedError() - - async def accept_knock(self, group_id, requester_user_id, content): - """Accept a users knock to the room. - - Errors if the user hasn't knocked, rather than inviting them. - """ - - await self.check_group_is_ours(group_id, requester_user_id, and_exists=True) - - raise NotImplementedError() - async def remove_user_from_group( - self, group_id, user_id, requester_user_id, content - ): + self, group_id: str, user_id: str, requester_user_id: str, content: JsonDict + ) -> JsonDict: """Remove a user from the group; either a user is leaving or an admin kicked them. """ @@ -745,6 +799,9 @@ async def remove_user_from_group( if is_kick: if self.hs.is_mine_id(user_id): groups_local = self.hs.get_groups_local_handler() + assert isinstance( + groups_local, GroupsLocalHandler + ), "Workers cannot remove users from groups." await groups_local.user_removed_from_group(group_id, user_id, {}) else: await self.transport_client.remove_user_from_group_notification( @@ -761,14 +818,15 @@ async def remove_user_from_group( return {} - async def create_group(self, group_id, requester_user_id, content): - group = await self.check_group_is_ours(group_id, requester_user_id) - + async def create_group( + self, group_id: str, requester_user_id: str, content: JsonDict + ) -> JsonDict: logger.info("Attempting to create group with ID: %r", group_id) # parsing the id into a GroupID validates it. group_id_obj = GroupID.from_string(group_id) + group = await self.check_group_is_ours(group_id, requester_user_id) if group: raise SynapseError(400, "Group already exists") @@ -813,7 +871,7 @@ async def create_group(self, group_id, requester_user_id, content): local_attestation = self.attestations.create_attestation( group_id, requester_user_id - ) + ) # type: Optional[JsonDict] else: local_attestation = None remote_attestation = None @@ -836,15 +894,14 @@ async def create_group(self, group_id, requester_user_id, content): return {"group_id": group_id} - async def delete_group(self, group_id, requester_user_id): + async def delete_group(self, group_id: str, requester_user_id: str) -> None: """Deletes a group, kicking out all current members. Only group admins or server admins can call this request Args: - group_id (str) - request_user_id (str) - + group_id: The group ID to delete. + requester_user_id: The user requesting to delete the group. """ await self.check_group_is_ours(group_id, requester_user_id, and_exists=True) @@ -867,6 +924,9 @@ async def delete_group(self, group_id, requester_user_id): async def _kick_user_from_group(user_id): if self.hs.is_mine_id(user_id): groups_local = self.hs.get_groups_local_handler() + assert isinstance( + groups_local, GroupsLocalHandler + ), "Workers cannot kick users from groups." await groups_local.user_removed_from_group(group_id, user_id, {}) else: await self.transport_client.remove_user_from_group_notification( @@ -898,7 +958,7 @@ async def _kick_user_from_group(user_id): await self.store.delete_group(group_id) -def _parse_join_policy_from_contents(content): +def _parse_join_policy_from_contents(content: JsonDict) -> Optional[str]: """Given a content for a request, return the specified join policy or None""" join_policy_dict = content.get("m.join_policy") @@ -908,7 +968,7 @@ def _parse_join_policy_from_contents(content): return None -def _parse_join_policy_dict(join_policy_dict): +def _parse_join_policy_dict(join_policy_dict: JsonDict) -> str: """Given a dict for the "m.join_policy" config return the join policy specified""" join_policy_type = join_policy_dict.get("type") if not join_policy_type: @@ -919,7 +979,7 @@ def _parse_join_policy_dict(join_policy_dict): return join_policy_type -def _parse_visibility_from_contents(content): +def _parse_visibility_from_contents(content: JsonDict) -> bool: """Given a content for a request parse out whether the entity should be public or not """ @@ -933,7 +993,7 @@ def _parse_visibility_from_contents(content): return is_public -def _parse_visibility_dict(visibility): +def _parse_visibility_dict(visibility: JsonDict) -> bool: """Given a dict for the "m.visibility" config return if the entity should be public or not """
synapse/rest/client/v2_alpha/groups.py+120 −23 modified@@ -16,11 +16,16 @@ import logging from functools import wraps -from typing import TYPE_CHECKING, Tuple +from typing import TYPE_CHECKING, Optional, Tuple from twisted.web.http import Request -from synapse.api.errors import SynapseError +from synapse.api.constants import ( + MAX_GROUP_CATEGORYID_LENGTH, + MAX_GROUP_ROLEID_LENGTH, + MAX_GROUPID_LENGTH, +) +from synapse.api.errors import Codes, SynapseError from synapse.handlers.groups_local import GroupsLocalHandler from synapse.http.servlet import ( RestServlet, @@ -84,7 +89,9 @@ async def on_POST(self, request: Request, group_id: str) -> Tuple[int, JsonDict] assert_params_in_dict( content, ("name", "avatar_url", "short_description", "long_description") ) - assert isinstance(self.groups_handler, GroupsLocalHandler) + assert isinstance( + self.groups_handler, GroupsLocalHandler + ), "Workers cannot create group profiles." await self.groups_handler.update_group_profile( group_id, requester_user_id, content ) @@ -137,13 +144,26 @@ def __init__(self, hs: "HomeServer"): @_validate_group_id async def on_PUT( - self, request: Request, group_id: str, category_id: str, room_id: str + self, request: Request, group_id: str, category_id: Optional[str], room_id: str ): requester = await self.auth.get_user_by_req(request) requester_user_id = requester.user.to_string() + if category_id == "": + raise SynapseError(400, "category_id cannot be empty", Codes.INVALID_PARAM) + + if category_id and len(category_id) > MAX_GROUP_CATEGORYID_LENGTH: + raise SynapseError( + 400, + "category_id may not be longer than %s characters" + % (MAX_GROUP_CATEGORYID_LENGTH,), + Codes.INVALID_PARAM, + ) + content = parse_json_object_from_request(request) - assert isinstance(self.groups_handler, GroupsLocalHandler) + assert isinstance( + self.groups_handler, GroupsLocalHandler + ), "Workers cannot modify group summaries." resp = await self.groups_handler.update_group_summary_room( group_id, requester_user_id, @@ -161,7 +181,9 @@ async def on_DELETE( requester = await self.auth.get_user_by_req(request) requester_user_id = requester.user.to_string() - assert isinstance(self.groups_handler, GroupsLocalHandler) + assert isinstance( + self.groups_handler, GroupsLocalHandler + ), "Workers cannot modify group profiles." resp = await self.groups_handler.delete_group_summary_room( group_id, requester_user_id, room_id=room_id, category_id=category_id ) @@ -202,8 +224,21 @@ async def on_PUT( requester = await self.auth.get_user_by_req(request) requester_user_id = requester.user.to_string() + if not category_id: + raise SynapseError(400, "category_id cannot be empty", Codes.INVALID_PARAM) + + if len(category_id) > MAX_GROUP_CATEGORYID_LENGTH: + raise SynapseError( + 400, + "category_id may not be longer than %s characters" + % (MAX_GROUP_CATEGORYID_LENGTH,), + Codes.INVALID_PARAM, + ) + content = parse_json_object_from_request(request) - assert isinstance(self.groups_handler, GroupsLocalHandler) + assert isinstance( + self.groups_handler, GroupsLocalHandler + ), "Workers cannot modify group categories." resp = await self.groups_handler.update_group_category( group_id, requester_user_id, category_id=category_id, content=content ) @@ -217,7 +252,9 @@ async def on_DELETE( requester = await self.auth.get_user_by_req(request) requester_user_id = requester.user.to_string() - assert isinstance(self.groups_handler, GroupsLocalHandler) + assert isinstance( + self.groups_handler, GroupsLocalHandler + ), "Workers cannot modify group categories." resp = await self.groups_handler.delete_group_category( group_id, requester_user_id, category_id=category_id ) @@ -279,8 +316,21 @@ async def on_PUT( requester = await self.auth.get_user_by_req(request) requester_user_id = requester.user.to_string() + if not role_id: + raise SynapseError(400, "role_id cannot be empty", Codes.INVALID_PARAM) + + if len(role_id) > MAX_GROUP_ROLEID_LENGTH: + raise SynapseError( + 400, + "role_id may not be longer than %s characters" + % (MAX_GROUP_ROLEID_LENGTH,), + Codes.INVALID_PARAM, + ) + content = parse_json_object_from_request(request) - assert isinstance(self.groups_handler, GroupsLocalHandler) + assert isinstance( + self.groups_handler, GroupsLocalHandler + ), "Workers cannot modify group roles." resp = await self.groups_handler.update_group_role( group_id, requester_user_id, role_id=role_id, content=content ) @@ -294,7 +344,9 @@ async def on_DELETE( requester = await self.auth.get_user_by_req(request) requester_user_id = requester.user.to_string() - assert isinstance(self.groups_handler, GroupsLocalHandler) + assert isinstance( + self.groups_handler, GroupsLocalHandler + ), "Workers cannot modify group roles." resp = await self.groups_handler.delete_group_role( group_id, requester_user_id, role_id=role_id ) @@ -347,13 +399,26 @@ def __init__(self, hs: "HomeServer"): @_validate_group_id async def on_PUT( - self, request: Request, group_id: str, role_id: str, user_id: str + self, request: Request, group_id: str, role_id: Optional[str], user_id: str ) -> Tuple[int, JsonDict]: requester = await self.auth.get_user_by_req(request) requester_user_id = requester.user.to_string() + if role_id == "": + raise SynapseError(400, "role_id cannot be empty", Codes.INVALID_PARAM) + + if role_id and len(role_id) > MAX_GROUP_ROLEID_LENGTH: + raise SynapseError( + 400, + "role_id may not be longer than %s characters" + % (MAX_GROUP_ROLEID_LENGTH,), + Codes.INVALID_PARAM, + ) + content = parse_json_object_from_request(request) - assert isinstance(self.groups_handler, GroupsLocalHandler) + assert isinstance( + self.groups_handler, GroupsLocalHandler + ), "Workers cannot modify group summaries." resp = await self.groups_handler.update_group_summary_user( group_id, requester_user_id, @@ -371,7 +436,9 @@ async def on_DELETE( requester = await self.auth.get_user_by_req(request) requester_user_id = requester.user.to_string() - assert isinstance(self.groups_handler, GroupsLocalHandler) + assert isinstance( + self.groups_handler, GroupsLocalHandler + ), "Workers cannot modify group summaries." resp = await self.groups_handler.delete_group_summary_user( group_id, requester_user_id, user_id=user_id, role_id=role_id ) @@ -465,7 +532,9 @@ async def on_PUT(self, request: Request, group_id: str) -> Tuple[int, JsonDict]: content = parse_json_object_from_request(request) - assert isinstance(self.groups_handler, GroupsLocalHandler) + assert isinstance( + self.groups_handler, GroupsLocalHandler + ), "Workers cannot modify group join policy." result = await self.groups_handler.set_group_join_policy( group_id, requester_user_id, content ) @@ -494,7 +563,19 @@ async def on_POST(self, request: Request) -> Tuple[int, JsonDict]: localpart = content.pop("localpart") group_id = GroupID(localpart, self.server_name).to_string() - assert isinstance(self.groups_handler, GroupsLocalHandler) + if not localpart: + raise SynapseError(400, "Group ID cannot be empty", Codes.INVALID_PARAM) + + if len(group_id) > MAX_GROUPID_LENGTH: + raise SynapseError( + 400, + "Group ID may not be longer than %s characters" % (MAX_GROUPID_LENGTH,), + Codes.INVALID_PARAM, + ) + + assert isinstance( + self.groups_handler, GroupsLocalHandler + ), "Workers cannot create groups." result = await self.groups_handler.create_group( group_id, requester_user_id, content ) @@ -523,7 +604,9 @@ async def on_PUT( requester_user_id = requester.user.to_string() content = parse_json_object_from_request(request) - assert isinstance(self.groups_handler, GroupsLocalHandler) + assert isinstance( + self.groups_handler, GroupsLocalHandler + ), "Workers cannot modify rooms in a group." result = await self.groups_handler.add_room_to_group( group_id, requester_user_id, room_id, content ) @@ -537,7 +620,9 @@ async def on_DELETE( requester = await self.auth.get_user_by_req(request) requester_user_id = requester.user.to_string() - assert isinstance(self.groups_handler, GroupsLocalHandler) + assert isinstance( + self.groups_handler, GroupsLocalHandler + ), "Workers cannot modify group categories." result = await self.groups_handler.remove_room_from_group( group_id, requester_user_id, room_id ) @@ -567,7 +652,9 @@ async def on_PUT( requester_user_id = requester.user.to_string() content = parse_json_object_from_request(request) - assert isinstance(self.groups_handler, GroupsLocalHandler) + assert isinstance( + self.groups_handler, GroupsLocalHandler + ), "Workers cannot modify group categories." result = await self.groups_handler.update_room_in_group( group_id, requester_user_id, room_id, config_key, content ) @@ -597,7 +684,9 @@ async def on_PUT(self, request: Request, group_id, user_id) -> Tuple[int, JsonDi content = parse_json_object_from_request(request) config = content.get("config", {}) - assert isinstance(self.groups_handler, GroupsLocalHandler) + assert isinstance( + self.groups_handler, GroupsLocalHandler + ), "Workers cannot invite users to a group." result = await self.groups_handler.invite( group_id, user_id, requester_user_id, config ) @@ -624,7 +713,9 @@ async def on_PUT(self, request: Request, group_id, user_id) -> Tuple[int, JsonDi requester_user_id = requester.user.to_string() content = parse_json_object_from_request(request) - assert isinstance(self.groups_handler, GroupsLocalHandler) + assert isinstance( + self.groups_handler, GroupsLocalHandler + ), "Workers cannot kick users from a group." result = await self.groups_handler.remove_user_from_group( group_id, user_id, requester_user_id, content ) @@ -649,7 +740,9 @@ async def on_PUT(self, request: Request, group_id: str) -> Tuple[int, JsonDict]: requester_user_id = requester.user.to_string() content = parse_json_object_from_request(request) - assert isinstance(self.groups_handler, GroupsLocalHandler) + assert isinstance( + self.groups_handler, GroupsLocalHandler + ), "Workers cannot leave a group for a users." result = await self.groups_handler.remove_user_from_group( group_id, requester_user_id, requester_user_id, content ) @@ -674,7 +767,9 @@ async def on_PUT(self, request: Request, group_id: str) -> Tuple[int, JsonDict]: requester_user_id = requester.user.to_string() content = parse_json_object_from_request(request) - assert isinstance(self.groups_handler, GroupsLocalHandler) + assert isinstance( + self.groups_handler, GroupsLocalHandler + ), "Workers cannot join a user to a group." result = await self.groups_handler.join_group( group_id, requester_user_id, content ) @@ -699,7 +794,9 @@ async def on_PUT(self, request: Request, group_id: str) -> Tuple[int, JsonDict]: requester_user_id = requester.user.to_string() content = parse_json_object_from_request(request) - assert isinstance(self.groups_handler, GroupsLocalHandler) + assert isinstance( + self.groups_handler, GroupsLocalHandler + ), "Workers cannot accept an invite to a group." result = await self.groups_handler.accept_invite( group_id, requester_user_id, content )
synapse/storage/databases/main/group_server.py+7 −2 modified@@ -14,7 +14,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Any, Dict, List, Optional, Tuple, Union +from typing import Any, Dict, List, Optional, Tuple + +from typing_extensions import TypedDict from synapse.api.errors import SynapseError from synapse.storage._base import SQLBaseStore, db_to_json @@ -26,6 +28,9 @@ _DEFAULT_CATEGORY_ID = "" _DEFAULT_ROLE_ID = "" +# A room in a group. +_RoomInGroup = TypedDict("_RoomInGroup", {"room_id": str, "is_public": bool}) + class GroupServerWorkerStore(SQLBaseStore): async def get_group(self, group_id: str) -> Optional[Dict[str, Any]]: @@ -72,7 +77,7 @@ async def get_invited_users_in_group(self, group_id: str) -> List[str]: async def get_rooms_in_group( self, group_id: str, include_private: bool = False - ) -> List[Dict[str, Union[str, bool]]]: + ) -> List[_RoomInGroup]: """Retrieve the rooms that belong to a given group. Does not return rooms that lack members.
3f58fc848d00Type hints and validation improvements. (#9321)
6 files changed · +177 −79
changelog.d/9321.bugfix+1 −0 added@@ -0,0 +1 @@ +Assert a maximum length for the `client_secret` parameter for spec compliance.
synapse/groups/groups_server.py+23 −2 modified@@ -18,6 +18,7 @@ import logging from synapse.api.errors import Codes, SynapseError +from synapse.handlers.profile import MAX_AVATAR_URL_LEN, MAX_DISPLAYNAME_LEN from synapse.types import GroupID, RoomID, UserID, get_domain_from_id from synapse.util.async_helpers import concurrently_execute @@ -32,6 +33,11 @@ # TODO: Flairs +# Note that the maximum lengths are somewhat arbitrary. +MAX_SHORT_DESC_LEN = 1000 +MAX_LONG_DESC_LEN = 10000 + + class GroupsServerWorkerHandler: def __init__(self, hs): self.hs = hs @@ -508,11 +514,26 @@ async def update_group_profile(self, group_id, requester_user_id, content): ) profile = {} - for keyname in ("name", "avatar_url", "short_description", "long_description"): + for keyname, max_length in ( + ("name", MAX_DISPLAYNAME_LEN), + ("avatar_url", MAX_AVATAR_URL_LEN), + ("short_description", MAX_SHORT_DESC_LEN), + ("long_description", MAX_LONG_DESC_LEN), + ): if keyname in content: value = content[keyname] if not isinstance(value, str): - raise SynapseError(400, "%r value is not a string" % (keyname,)) + raise SynapseError( + 400, + "%r value is not a string" % (keyname,), + errcode=Codes.INVALID_PARAM, + ) + if len(value) > max_length: + raise SynapseError( + 400, + "Invalid %s parameter" % (keyname,), + errcode=Codes.INVALID_PARAM, + ) profile[keyname] = value await self.store.update_group_profile(group_id, profile)
synapse/rest/client/v2_alpha/groups.py+119 −60 modified@@ -16,13 +16,24 @@ import logging from functools import wraps +from typing import TYPE_CHECKING, Tuple + +from twisted.web.http import Request from synapse.api.errors import SynapseError -from synapse.http.servlet import RestServlet, parse_json_object_from_request -from synapse.types import GroupID +from synapse.handlers.groups_local import GroupsLocalHandler +from synapse.http.servlet import ( + RestServlet, + assert_params_in_dict, + parse_json_object_from_request, +) +from synapse.types import GroupID, JsonDict from ._base import client_patterns +if TYPE_CHECKING: + from synapse.app.homeserver import HomeServer + logger = logging.getLogger(__name__) @@ -33,7 +44,7 @@ def _validate_group_id(f): """ @wraps(f) - def wrapper(self, request, group_id, *args, **kwargs): + def wrapper(self, request: Request, group_id: str, *args, **kwargs): if not GroupID.is_valid(group_id): raise SynapseError(400, "%s is not a legal group ID" % (group_id,)) @@ -48,14 +59,14 @@ class GroupServlet(RestServlet): PATTERNS = client_patterns("/groups/(?P<group_id>[^/]*)/profile$") - def __init__(self, hs): + def __init__(self, hs: "HomeServer"): super().__init__() self.auth = hs.get_auth() self.clock = hs.get_clock() self.groups_handler = hs.get_groups_local_handler() @_validate_group_id - async def on_GET(self, request, group_id): + async def on_GET(self, request: Request, group_id: str) -> Tuple[int, JsonDict]: requester = await self.auth.get_user_by_req(request, allow_guest=True) requester_user_id = requester.user.to_string() @@ -66,11 +77,15 @@ async def on_GET(self, request, group_id): return 200, group_description @_validate_group_id - async def on_POST(self, request, group_id): + async def on_POST(self, request: Request, group_id: str) -> Tuple[int, JsonDict]: requester = await self.auth.get_user_by_req(request) requester_user_id = requester.user.to_string() content = parse_json_object_from_request(request) + assert_params_in_dict( + content, ("name", "avatar_url", "short_description", "long_description") + ) + assert isinstance(self.groups_handler, GroupsLocalHandler) await self.groups_handler.update_group_profile( group_id, requester_user_id, content ) @@ -84,14 +99,14 @@ class GroupSummaryServlet(RestServlet): PATTERNS = client_patterns("/groups/(?P<group_id>[^/]*)/summary$") - def __init__(self, hs): + def __init__(self, hs: "HomeServer"): super().__init__() self.auth = hs.get_auth() self.clock = hs.get_clock() self.groups_handler = hs.get_groups_local_handler() @_validate_group_id - async def on_GET(self, request, group_id): + async def on_GET(self, request: Request, group_id: str) -> Tuple[int, JsonDict]: requester = await self.auth.get_user_by_req(request, allow_guest=True) requester_user_id = requester.user.to_string() @@ -116,18 +131,21 @@ class GroupSummaryRoomsCatServlet(RestServlet): "/rooms/(?P<room_id>[^/]*)$" ) - def __init__(self, hs): + def __init__(self, hs: "HomeServer"): super().__init__() self.auth = hs.get_auth() self.clock = hs.get_clock() self.groups_handler = hs.get_groups_local_handler() @_validate_group_id - async def on_PUT(self, request, group_id, category_id, room_id): + async def on_PUT( + self, request: Request, group_id: str, category_id: str, room_id: str + ): requester = await self.auth.get_user_by_req(request) requester_user_id = requester.user.to_string() content = parse_json_object_from_request(request) + assert isinstance(self.groups_handler, GroupsLocalHandler) resp = await self.groups_handler.update_group_summary_room( group_id, requester_user_id, @@ -139,10 +157,13 @@ async def on_PUT(self, request, group_id, category_id, room_id): return 200, resp @_validate_group_id - async def on_DELETE(self, request, group_id, category_id, room_id): + async def on_DELETE( + self, request: Request, group_id: str, category_id: str, room_id: str + ): requester = await self.auth.get_user_by_req(request) requester_user_id = requester.user.to_string() + assert isinstance(self.groups_handler, GroupsLocalHandler) resp = await self.groups_handler.delete_group_summary_room( group_id, requester_user_id, room_id=room_id, category_id=category_id ) @@ -158,14 +179,16 @@ class GroupCategoryServlet(RestServlet): "/groups/(?P<group_id>[^/]*)/categories/(?P<category_id>[^/]+)$" ) - def __init__(self, hs): + def __init__(self, hs: "HomeServer"): super().__init__() self.auth = hs.get_auth() self.clock = hs.get_clock() self.groups_handler = hs.get_groups_local_handler() @_validate_group_id - async def on_GET(self, request, group_id, category_id): + async def on_GET( + self, request: Request, group_id: str, category_id: str + ) -> Tuple[int, JsonDict]: requester = await self.auth.get_user_by_req(request, allow_guest=True) requester_user_id = requester.user.to_string() @@ -176,22 +199,28 @@ async def on_GET(self, request, group_id, category_id): return 200, category @_validate_group_id - async def on_PUT(self, request, group_id, category_id): + async def on_PUT( + self, request: Request, group_id: str, category_id: str + ) -> Tuple[int, JsonDict]: requester = await self.auth.get_user_by_req(request) requester_user_id = requester.user.to_string() content = parse_json_object_from_request(request) + assert isinstance(self.groups_handler, GroupsLocalHandler) resp = await self.groups_handler.update_group_category( group_id, requester_user_id, category_id=category_id, content=content ) return 200, resp @_validate_group_id - async def on_DELETE(self, request, group_id, category_id): + async def on_DELETE( + self, request: Request, group_id: str, category_id: str + ) -> Tuple[int, JsonDict]: requester = await self.auth.get_user_by_req(request) requester_user_id = requester.user.to_string() + assert isinstance(self.groups_handler, GroupsLocalHandler) resp = await self.groups_handler.delete_group_category( group_id, requester_user_id, category_id=category_id ) @@ -205,14 +234,14 @@ class GroupCategoriesServlet(RestServlet): PATTERNS = client_patterns("/groups/(?P<group_id>[^/]*)/categories/$") - def __init__(self, hs): + def __init__(self, hs: "HomeServer"): super().__init__() self.auth = hs.get_auth() self.clock = hs.get_clock() self.groups_handler = hs.get_groups_local_handler() @_validate_group_id - async def on_GET(self, request, group_id): + async def on_GET(self, request: Request, group_id: str) -> Tuple[int, JsonDict]: requester = await self.auth.get_user_by_req(request, allow_guest=True) requester_user_id = requester.user.to_string() @@ -229,14 +258,16 @@ class GroupRoleServlet(RestServlet): PATTERNS = client_patterns("/groups/(?P<group_id>[^/]*)/roles/(?P<role_id>[^/]+)$") - def __init__(self, hs): + def __init__(self, hs: "HomeServer"): super().__init__() self.auth = hs.get_auth() self.clock = hs.get_clock() self.groups_handler = hs.get_groups_local_handler() @_validate_group_id - async def on_GET(self, request, group_id, role_id): + async def on_GET( + self, request: Request, group_id: str, role_id: str + ) -> Tuple[int, JsonDict]: requester = await self.auth.get_user_by_req(request, allow_guest=True) requester_user_id = requester.user.to_string() @@ -247,22 +278,28 @@ async def on_GET(self, request, group_id, role_id): return 200, category @_validate_group_id - async def on_PUT(self, request, group_id, role_id): + async def on_PUT( + self, request: Request, group_id: str, role_id: str + ) -> Tuple[int, JsonDict]: requester = await self.auth.get_user_by_req(request) requester_user_id = requester.user.to_string() content = parse_json_object_from_request(request) + assert isinstance(self.groups_handler, GroupsLocalHandler) resp = await self.groups_handler.update_group_role( group_id, requester_user_id, role_id=role_id, content=content ) return 200, resp @_validate_group_id - async def on_DELETE(self, request, group_id, role_id): + async def on_DELETE( + self, request: Request, group_id: str, role_id: str + ) -> Tuple[int, JsonDict]: requester = await self.auth.get_user_by_req(request) requester_user_id = requester.user.to_string() + assert isinstance(self.groups_handler, GroupsLocalHandler) resp = await self.groups_handler.delete_group_role( group_id, requester_user_id, role_id=role_id ) @@ -276,14 +313,14 @@ class GroupRolesServlet(RestServlet): PATTERNS = client_patterns("/groups/(?P<group_id>[^/]*)/roles/$") - def __init__(self, hs): + def __init__(self, hs: "HomeServer"): super().__init__() self.auth = hs.get_auth() self.clock = hs.get_clock() self.groups_handler = hs.get_groups_local_handler() @_validate_group_id - async def on_GET(self, request, group_id): + async def on_GET(self, request: Request, group_id: str) -> Tuple[int, JsonDict]: requester = await self.auth.get_user_by_req(request, allow_guest=True) requester_user_id = requester.user.to_string() @@ -308,18 +345,21 @@ class GroupSummaryUsersRoleServlet(RestServlet): "/users/(?P<user_id>[^/]*)$" ) - def __init__(self, hs): + def __init__(self, hs: "HomeServer"): super().__init__() self.auth = hs.get_auth() self.clock = hs.get_clock() self.groups_handler = hs.get_groups_local_handler() @_validate_group_id - async def on_PUT(self, request, group_id, role_id, user_id): + async def on_PUT( + self, request: Request, group_id: str, role_id: str, user_id: str + ) -> Tuple[int, JsonDict]: requester = await self.auth.get_user_by_req(request) requester_user_id = requester.user.to_string() content = parse_json_object_from_request(request) + assert isinstance(self.groups_handler, GroupsLocalHandler) resp = await self.groups_handler.update_group_summary_user( group_id, requester_user_id, @@ -331,10 +371,13 @@ async def on_PUT(self, request, group_id, role_id, user_id): return 200, resp @_validate_group_id - async def on_DELETE(self, request, group_id, role_id, user_id): + async def on_DELETE( + self, request: Request, group_id: str, role_id: str, user_id: str + ): requester = await self.auth.get_user_by_req(request) requester_user_id = requester.user.to_string() + assert isinstance(self.groups_handler, GroupsLocalHandler) resp = await self.groups_handler.delete_group_summary_user( group_id, requester_user_id, user_id=user_id, role_id=role_id ) @@ -348,14 +391,14 @@ class GroupRoomServlet(RestServlet): PATTERNS = client_patterns("/groups/(?P<group_id>[^/]*)/rooms$") - def __init__(self, hs): + def __init__(self, hs: "HomeServer"): super().__init__() self.auth = hs.get_auth() self.clock = hs.get_clock() self.groups_handler = hs.get_groups_local_handler() @_validate_group_id - async def on_GET(self, request, group_id): + async def on_GET(self, request: Request, group_id: str) -> Tuple[int, JsonDict]: requester = await self.auth.get_user_by_req(request, allow_guest=True) requester_user_id = requester.user.to_string() @@ -372,14 +415,14 @@ class GroupUsersServlet(RestServlet): PATTERNS = client_patterns("/groups/(?P<group_id>[^/]*)/users$") - def __init__(self, hs): + def __init__(self, hs: "HomeServer"): super().__init__() self.auth = hs.get_auth() self.clock = hs.get_clock() self.groups_handler = hs.get_groups_local_handler() @_validate_group_id - async def on_GET(self, request, group_id): + async def on_GET(self, request: Request, group_id: str) -> Tuple[int, JsonDict]: requester = await self.auth.get_user_by_req(request, allow_guest=True) requester_user_id = requester.user.to_string() @@ -396,14 +439,14 @@ class GroupInvitedUsersServlet(RestServlet): PATTERNS = client_patterns("/groups/(?P<group_id>[^/]*)/invited_users$") - def __init__(self, hs): + def __init__(self, hs: "HomeServer"): super().__init__() self.auth = hs.get_auth() self.clock = hs.get_clock() self.groups_handler = hs.get_groups_local_handler() @_validate_group_id - async def on_GET(self, request, group_id): + async def on_GET(self, request: Request, group_id: str) -> Tuple[int, JsonDict]: requester = await self.auth.get_user_by_req(request) requester_user_id = requester.user.to_string() @@ -420,18 +463,19 @@ class GroupSettingJoinPolicyServlet(RestServlet): PATTERNS = client_patterns("/groups/(?P<group_id>[^/]*)/settings/m.join_policy$") - def __init__(self, hs): + def __init__(self, hs: "HomeServer"): super().__init__() self.auth = hs.get_auth() self.groups_handler = hs.get_groups_local_handler() @_validate_group_id - async def on_PUT(self, request, group_id): + async def on_PUT(self, request: Request, group_id: str) -> Tuple[int, JsonDict]: requester = await self.auth.get_user_by_req(request) requester_user_id = requester.user.to_string() content = parse_json_object_from_request(request) + assert isinstance(self.groups_handler, GroupsLocalHandler) result = await self.groups_handler.set_group_join_policy( group_id, requester_user_id, content ) @@ -445,14 +489,14 @@ class GroupCreateServlet(RestServlet): PATTERNS = client_patterns("/create_group$") - def __init__(self, hs): + def __init__(self, hs: "HomeServer"): super().__init__() self.auth = hs.get_auth() self.clock = hs.get_clock() self.groups_handler = hs.get_groups_local_handler() self.server_name = hs.hostname - async def on_POST(self, request): + async def on_POST(self, request: Request) -> Tuple[int, JsonDict]: requester = await self.auth.get_user_by_req(request) requester_user_id = requester.user.to_string() @@ -461,6 +505,7 @@ async def on_POST(self, request): localpart = content.pop("localpart") group_id = GroupID(localpart, self.server_name).to_string() + assert isinstance(self.groups_handler, GroupsLocalHandler) result = await self.groups_handler.create_group( group_id, requester_user_id, content ) @@ -476,29 +521,35 @@ class GroupAdminRoomsServlet(RestServlet): "/groups/(?P<group_id>[^/]*)/admin/rooms/(?P<room_id>[^/]*)$" ) - def __init__(self, hs): + def __init__(self, hs: "HomeServer"): super().__init__() self.auth = hs.get_auth() self.clock = hs.get_clock() self.groups_handler = hs.get_groups_local_handler() @_validate_group_id - async def on_PUT(self, request, group_id, room_id): + async def on_PUT( + self, request: Request, group_id: str, room_id: str + ) -> Tuple[int, JsonDict]: requester = await self.auth.get_user_by_req(request) requester_user_id = requester.user.to_string() content = parse_json_object_from_request(request) + assert isinstance(self.groups_handler, GroupsLocalHandler) result = await self.groups_handler.add_room_to_group( group_id, requester_user_id, room_id, content ) return 200, result @_validate_group_id - async def on_DELETE(self, request, group_id, room_id): + async def on_DELETE( + self, request: Request, group_id: str, room_id: str + ) -> Tuple[int, JsonDict]: requester = await self.auth.get_user_by_req(request) requester_user_id = requester.user.to_string() + assert isinstance(self.groups_handler, GroupsLocalHandler) result = await self.groups_handler.remove_room_from_group( group_id, requester_user_id, room_id ) @@ -515,18 +566,21 @@ class GroupAdminRoomsConfigServlet(RestServlet): "/config/(?P<config_key>[^/]*)$" ) - def __init__(self, hs): + def __init__(self, hs: "HomeServer"): super().__init__() self.auth = hs.get_auth() self.clock = hs.get_clock() self.groups_handler = hs.get_groups_local_handler() @_validate_group_id - async def on_PUT(self, request, group_id, room_id, config_key): + async def on_PUT( + self, request: Request, group_id: str, room_id: str, config_key: str + ): requester = await self.auth.get_user_by_req(request) requester_user_id = requester.user.to_string() content = parse_json_object_from_request(request) + assert isinstance(self.groups_handler, GroupsLocalHandler) result = await self.groups_handler.update_room_in_group( group_id, requester_user_id, room_id, config_key, content ) @@ -542,7 +596,7 @@ class GroupAdminUsersInviteServlet(RestServlet): "/groups/(?P<group_id>[^/]*)/admin/users/invite/(?P<user_id>[^/]*)$" ) - def __init__(self, hs): + def __init__(self, hs: "HomeServer"): super().__init__() self.auth = hs.get_auth() self.clock = hs.get_clock() @@ -551,12 +605,13 @@ def __init__(self, hs): self.is_mine_id = hs.is_mine_id @_validate_group_id - async def on_PUT(self, request, group_id, user_id): + async def on_PUT(self, request: Request, group_id, user_id) -> Tuple[int, JsonDict]: requester = await self.auth.get_user_by_req(request) requester_user_id = requester.user.to_string() content = parse_json_object_from_request(request) config = content.get("config", {}) + assert isinstance(self.groups_handler, GroupsLocalHandler) result = await self.groups_handler.invite( group_id, user_id, requester_user_id, config ) @@ -572,18 +627,19 @@ class GroupAdminUsersKickServlet(RestServlet): "/groups/(?P<group_id>[^/]*)/admin/users/remove/(?P<user_id>[^/]*)$" ) - def __init__(self, hs): + def __init__(self, hs: "HomeServer"): super().__init__() self.auth = hs.get_auth() self.clock = hs.get_clock() self.groups_handler = hs.get_groups_local_handler() @_validate_group_id - async def on_PUT(self, request, group_id, user_id): + async def on_PUT(self, request: Request, group_id, user_id) -> Tuple[int, JsonDict]: requester = await self.auth.get_user_by_req(request) requester_user_id = requester.user.to_string() content = parse_json_object_from_request(request) + assert isinstance(self.groups_handler, GroupsLocalHandler) result = await self.groups_handler.remove_user_from_group( group_id, user_id, requester_user_id, content ) @@ -597,18 +653,19 @@ class GroupSelfLeaveServlet(RestServlet): PATTERNS = client_patterns("/groups/(?P<group_id>[^/]*)/self/leave$") - def __init__(self, hs): + def __init__(self, hs: "HomeServer"): super().__init__() self.auth = hs.get_auth() self.clock = hs.get_clock() self.groups_handler = hs.get_groups_local_handler() @_validate_group_id - async def on_PUT(self, request, group_id): + async def on_PUT(self, request: Request, group_id: str) -> Tuple[int, JsonDict]: requester = await self.auth.get_user_by_req(request) requester_user_id = requester.user.to_string() content = parse_json_object_from_request(request) + assert isinstance(self.groups_handler, GroupsLocalHandler) result = await self.groups_handler.remove_user_from_group( group_id, requester_user_id, requester_user_id, content ) @@ -622,18 +679,19 @@ class GroupSelfJoinServlet(RestServlet): PATTERNS = client_patterns("/groups/(?P<group_id>[^/]*)/self/join$") - def __init__(self, hs): + def __init__(self, hs: "HomeServer"): super().__init__() self.auth = hs.get_auth() self.clock = hs.get_clock() self.groups_handler = hs.get_groups_local_handler() @_validate_group_id - async def on_PUT(self, request, group_id): + async def on_PUT(self, request: Request, group_id: str) -> Tuple[int, JsonDict]: requester = await self.auth.get_user_by_req(request) requester_user_id = requester.user.to_string() content = parse_json_object_from_request(request) + assert isinstance(self.groups_handler, GroupsLocalHandler) result = await self.groups_handler.join_group( group_id, requester_user_id, content ) @@ -647,18 +705,19 @@ class GroupSelfAcceptInviteServlet(RestServlet): PATTERNS = client_patterns("/groups/(?P<group_id>[^/]*)/self/accept_invite$") - def __init__(self, hs): + def __init__(self, hs: "HomeServer"): super().__init__() self.auth = hs.get_auth() self.clock = hs.get_clock() self.groups_handler = hs.get_groups_local_handler() @_validate_group_id - async def on_PUT(self, request, group_id): + async def on_PUT(self, request: Request, group_id: str) -> Tuple[int, JsonDict]: requester = await self.auth.get_user_by_req(request) requester_user_id = requester.user.to_string() content = parse_json_object_from_request(request) + assert isinstance(self.groups_handler, GroupsLocalHandler) result = await self.groups_handler.accept_invite( group_id, requester_user_id, content ) @@ -672,14 +731,14 @@ class GroupSelfUpdatePublicityServlet(RestServlet): PATTERNS = client_patterns("/groups/(?P<group_id>[^/]*)/self/update_publicity$") - def __init__(self, hs): + def __init__(self, hs: "HomeServer"): super().__init__() self.auth = hs.get_auth() self.clock = hs.get_clock() self.store = hs.get_datastore() @_validate_group_id - async def on_PUT(self, request, group_id): + async def on_PUT(self, request: Request, group_id: str) -> Tuple[int, JsonDict]: requester = await self.auth.get_user_by_req(request) requester_user_id = requester.user.to_string() @@ -696,14 +755,14 @@ class PublicisedGroupsForUserServlet(RestServlet): PATTERNS = client_patterns("/publicised_groups/(?P<user_id>[^/]*)$") - def __init__(self, hs): + def __init__(self, hs: "HomeServer"): super().__init__() self.auth = hs.get_auth() self.clock = hs.get_clock() self.store = hs.get_datastore() self.groups_handler = hs.get_groups_local_handler() - async def on_GET(self, request, user_id): + async def on_GET(self, request: Request, user_id: str) -> Tuple[int, JsonDict]: await self.auth.get_user_by_req(request, allow_guest=True) result = await self.groups_handler.get_publicised_groups_for_user(user_id) @@ -717,14 +776,14 @@ class PublicisedGroupsForUsersServlet(RestServlet): PATTERNS = client_patterns("/publicised_groups$") - def __init__(self, hs): + def __init__(self, hs: "HomeServer"): super().__init__() self.auth = hs.get_auth() self.clock = hs.get_clock() self.store = hs.get_datastore() self.groups_handler = hs.get_groups_local_handler() - async def on_POST(self, request): + async def on_POST(self, request: Request) -> Tuple[int, JsonDict]: await self.auth.get_user_by_req(request, allow_guest=True) content = parse_json_object_from_request(request) @@ -741,13 +800,13 @@ class GroupsForUserServlet(RestServlet): PATTERNS = client_patterns("/joined_groups$") - def __init__(self, hs): + def __init__(self, hs: "HomeServer"): super().__init__() self.auth = hs.get_auth() self.clock = hs.get_clock() self.groups_handler = hs.get_groups_local_handler() - async def on_GET(self, request): + async def on_GET(self, request: Request) -> Tuple[int, JsonDict]: requester = await self.auth.get_user_by_req(request, allow_guest=True) requester_user_id = requester.user.to_string() @@ -756,7 +815,7 @@ async def on_GET(self, request): return 200, result -def register_servlets(hs, http_server): +def register_servlets(hs: "HomeServer", http_server): GroupServlet(hs).register(http_server) GroupSummaryServlet(hs).register(http_server) GroupInvitedUsersServlet(hs).register(http_server)
synapse/rest/client/v2_alpha/register.py+2 −0 modified@@ -193,6 +193,7 @@ async def on_POST(self, request): body, ["client_secret", "country", "phone_number", "send_attempt"] ) client_secret = body["client_secret"] + assert_valid_client_secret(client_secret) country = body["country"] phone_number = body["phone_number"] send_attempt = body["send_attempt"] @@ -293,6 +294,7 @@ async def on_GET(self, request, medium): sid = parse_string(request, "sid", required=True) client_secret = parse_string(request, "client_secret", required=True) + assert_valid_client_secret(client_secret) token = parse_string(request, "token", required=True) # Attempt to validate a 3PID session
synapse/server.py+14 −2 modified@@ -25,7 +25,17 @@ import functools import logging import os -from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, TypeVar, cast +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + List, + Optional, + TypeVar, + Union, + cast, +) import twisted.internet.base import twisted.internet.tcp @@ -588,7 +598,9 @@ def get_user_directory_handler(self) -> UserDirectoryHandler: return UserDirectoryHandler(self) @cache_in_self - def get_groups_local_handler(self): + def get_groups_local_handler( + self, + ) -> Union[GroupsLocalWorkerHandler, GroupsLocalHandler]: if self.config.worker_app: return GroupsLocalWorkerHandler(self) else:
synapse/util/stringutils.py+18 −15 modified@@ -25,7 +25,7 @@ _string_with_symbols = string.digits + string.ascii_letters + ".,;:^&*-_+=#~@" # https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-register-email-requesttoken -client_secret_regex = re.compile(r"^[0-9a-zA-Z\.\=\_\-]+$") +CLIENT_SECRET_REGEX = re.compile(r"^[0-9a-zA-Z\.=_\-]+$") # https://matrix.org/docs/spec/client_server/r0.6.1#matrix-content-mxc-uris, # together with https://github.com/matrix-org/matrix-doc/issues/2177 which basically @@ -42,28 +42,31 @@ rand = random.SystemRandom() -def random_string(length): +def random_string(length: int) -> str: return "".join(rand.choice(string.ascii_letters) for _ in range(length)) -def random_string_with_symbols(length): +def random_string_with_symbols(length: int) -> str: return "".join(rand.choice(_string_with_symbols) for _ in range(length)) -def is_ascii(s): - if isinstance(s, bytes): - try: - s.decode("ascii").encode("ascii") - except UnicodeDecodeError: - return False - except UnicodeEncodeError: - return False - return True +def is_ascii(s: bytes) -> bool: + try: + s.decode("ascii").encode("ascii") + except UnicodeDecodeError: + return False + except UnicodeEncodeError: + return False + return True -def assert_valid_client_secret(client_secret): - """Validate that a given string matches the client_secret regex defined by the spec""" - if client_secret_regex.match(client_secret) is None: +def assert_valid_client_secret(client_secret: str) -> None: + """Validate that a given string matches the client_secret defined by the spec""" + if ( + len(client_secret) <= 0 + or len(client_secret) > 255 + or CLIENT_SECRET_REGEX.match(client_secret) is None + ): raise SynapseError( 400, "Invalid client_secret parameter", errcode=Codes.INVALID_PARAM )
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
12- github.com/advisories/GHSA-jrh7-mhhx-6h88ghsaADVISORY
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/TNNAJOZNMVMXM6AS7RFFKB4QLUJ4IFEY/mitrevendor-advisoryx_refsource_FEDORA
- nvd.nist.gov/vuln/detail/CVE-2021-21393ghsaADVISORY
- github.com/matrix-org/synapse/commit/3f58fc848d0002de4605bed91603a1f9f245d128ghsaWEB
- github.com/matrix-org/synapse/commit/d2f0ec12d5c8f113095408888e87e191ac546499ghsaWEB
- github.com/matrix-org/synapse/pull/9321ghsax_refsource_MISCWEB
- github.com/matrix-org/synapse/pull/9393ghsax_refsource_MISCWEB
- github.com/matrix-org/synapse/security/advisories/GHSA-jrh7-mhhx-6h88ghsax_refsource_CONFIRMWEB
- github.com/pypa/advisory-database/tree/main/vulns/matrix-synapse/PYSEC-2021-26.yamlghsaWEB
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/TNNAJOZNMVMXM6AS7RFFKB4QLUJ4IFEYghsaWEB
- pypi.org/project/matrix-synapseghsaWEB
- pypi.org/project/matrix-synapse/mitrex_refsource_MISC
News mentions
0No linked articles in our index yet.