mirror of
https://github.com/element-hq/synapse.git
synced 2026-05-14 16:55:08 +00:00
Sanitize invite_room_state received over federation
This commit is contained in:
@@ -157,6 +157,21 @@ pub struct RoomVersion {
|
||||
/// This is similar to how doubly-linked lists can potentially not refer to previous items correctly
|
||||
/// without verifying the list's integrity, but doing it on every insert is too expensive.
|
||||
pub msc4242_state_dags: bool,
|
||||
/// Whether the `m.room.create` event is required in the
|
||||
/// `invite_state`/`knock_state` and `invite_room_state`/`knock_room_state` in the
|
||||
/// client and federation API's.
|
||||
///
|
||||
/// Also determines whether full PDU's are returned in the
|
||||
/// `invite_room_state`/`knock_room_state` in the federation API. The client API
|
||||
/// still uses stripped state.
|
||||
///
|
||||
/// According to MSC4311:
|
||||
/// > If any of the events are not a PDU, not for the room ID specified, or fail
|
||||
/// > signature checks, or the `m.room.create` event is missing, the receiving
|
||||
/// > server MAY respond to invites with a `400 M_MISSING_PARAM` standard Matrix
|
||||
/// > error (new to the endpoint). For invites to room version 12+ rooms, servers
|
||||
/// > SHOULD rather than MAY respond to such requests with `400 M_MISSING_PARAM`.
|
||||
pub msc4311_stripped_state: bool,
|
||||
}
|
||||
|
||||
const ROOM_VERSION_V1: RoomVersion = RoomVersion {
|
||||
@@ -182,6 +197,7 @@ const ROOM_VERSION_V1: RoomVersion = RoomVersion {
|
||||
msc4291_room_ids_as_hashes: false,
|
||||
strict_event_byte_limits_room_versions: false,
|
||||
msc4242_state_dags: false,
|
||||
msc4311_stripped_state: false,
|
||||
};
|
||||
|
||||
const ROOM_VERSION_V2: RoomVersion = RoomVersion {
|
||||
@@ -207,6 +223,7 @@ const ROOM_VERSION_V2: RoomVersion = RoomVersion {
|
||||
msc4291_room_ids_as_hashes: false,
|
||||
strict_event_byte_limits_room_versions: false,
|
||||
msc4242_state_dags: false,
|
||||
msc4311_stripped_state: false,
|
||||
};
|
||||
|
||||
const ROOM_VERSION_V3: RoomVersion = RoomVersion {
|
||||
@@ -232,6 +249,7 @@ const ROOM_VERSION_V3: RoomVersion = RoomVersion {
|
||||
msc4291_room_ids_as_hashes: false,
|
||||
strict_event_byte_limits_room_versions: false,
|
||||
msc4242_state_dags: false,
|
||||
msc4311_stripped_state: false,
|
||||
};
|
||||
|
||||
const ROOM_VERSION_V4: RoomVersion = RoomVersion {
|
||||
@@ -257,6 +275,7 @@ const ROOM_VERSION_V4: RoomVersion = RoomVersion {
|
||||
msc4291_room_ids_as_hashes: false,
|
||||
strict_event_byte_limits_room_versions: false,
|
||||
msc4242_state_dags: false,
|
||||
msc4311_stripped_state: false,
|
||||
};
|
||||
|
||||
const ROOM_VERSION_V5: RoomVersion = RoomVersion {
|
||||
@@ -282,6 +301,7 @@ const ROOM_VERSION_V5: RoomVersion = RoomVersion {
|
||||
msc4291_room_ids_as_hashes: false,
|
||||
strict_event_byte_limits_room_versions: false,
|
||||
msc4242_state_dags: false,
|
||||
msc4311_stripped_state: false,
|
||||
};
|
||||
|
||||
const ROOM_VERSION_V6: RoomVersion = RoomVersion {
|
||||
@@ -307,6 +327,7 @@ const ROOM_VERSION_V6: RoomVersion = RoomVersion {
|
||||
msc4291_room_ids_as_hashes: false,
|
||||
strict_event_byte_limits_room_versions: false,
|
||||
msc4242_state_dags: false,
|
||||
msc4311_stripped_state: false,
|
||||
};
|
||||
|
||||
const ROOM_VERSION_V7: RoomVersion = RoomVersion {
|
||||
@@ -332,6 +353,7 @@ const ROOM_VERSION_V7: RoomVersion = RoomVersion {
|
||||
msc4291_room_ids_as_hashes: false,
|
||||
strict_event_byte_limits_room_versions: false,
|
||||
msc4242_state_dags: false,
|
||||
msc4311_stripped_state: false,
|
||||
};
|
||||
|
||||
const ROOM_VERSION_V8: RoomVersion = RoomVersion {
|
||||
@@ -357,6 +379,7 @@ const ROOM_VERSION_V8: RoomVersion = RoomVersion {
|
||||
msc4291_room_ids_as_hashes: false,
|
||||
strict_event_byte_limits_room_versions: false,
|
||||
msc4242_state_dags: false,
|
||||
msc4311_stripped_state: false,
|
||||
};
|
||||
|
||||
const ROOM_VERSION_V9: RoomVersion = RoomVersion {
|
||||
@@ -382,6 +405,7 @@ const ROOM_VERSION_V9: RoomVersion = RoomVersion {
|
||||
msc4291_room_ids_as_hashes: false,
|
||||
strict_event_byte_limits_room_versions: false,
|
||||
msc4242_state_dags: false,
|
||||
msc4311_stripped_state: false,
|
||||
};
|
||||
|
||||
const ROOM_VERSION_V10: RoomVersion = RoomVersion {
|
||||
@@ -407,6 +431,7 @@ const ROOM_VERSION_V10: RoomVersion = RoomVersion {
|
||||
msc4291_room_ids_as_hashes: false,
|
||||
strict_event_byte_limits_room_versions: false,
|
||||
msc4242_state_dags: false,
|
||||
msc4311_stripped_state: false,
|
||||
};
|
||||
|
||||
/// MSC3389 (Redaction changes for events with a relation) based on room version "10".
|
||||
@@ -433,6 +458,7 @@ const ROOM_VERSION_MSC3389V10: RoomVersion = RoomVersion {
|
||||
msc4291_room_ids_as_hashes: false,
|
||||
strict_event_byte_limits_room_versions: true,
|
||||
msc4242_state_dags: false,
|
||||
msc4311_stripped_state: false,
|
||||
};
|
||||
|
||||
/// MSC1767 (Extensible Events) based on room version "10".
|
||||
@@ -459,6 +485,7 @@ const ROOM_VERSION_MSC1767V10: RoomVersion = RoomVersion {
|
||||
msc4291_room_ids_as_hashes: false,
|
||||
strict_event_byte_limits_room_versions: false,
|
||||
msc4242_state_dags: false,
|
||||
msc4311_stripped_state: false,
|
||||
};
|
||||
|
||||
/// MSC3757 (Restricting who can overwrite a state event) based on room version "10".
|
||||
@@ -485,6 +512,7 @@ const ROOM_VERSION_MSC3757V10: RoomVersion = RoomVersion {
|
||||
msc4291_room_ids_as_hashes: false,
|
||||
strict_event_byte_limits_room_versions: false,
|
||||
msc4242_state_dags: false,
|
||||
msc4311_stripped_state: false,
|
||||
};
|
||||
|
||||
const ROOM_VERSION_V11: RoomVersion = RoomVersion {
|
||||
@@ -510,6 +538,7 @@ const ROOM_VERSION_V11: RoomVersion = RoomVersion {
|
||||
msc4291_room_ids_as_hashes: false,
|
||||
strict_event_byte_limits_room_versions: true, // Changed from v10
|
||||
msc4242_state_dags: false,
|
||||
msc4311_stripped_state: false,
|
||||
};
|
||||
|
||||
/// MSC3757 (Restricting who can overwrite a state event) based on room version "11".
|
||||
@@ -536,6 +565,7 @@ const ROOM_VERSION_MSC3757V11: RoomVersion = RoomVersion {
|
||||
msc4291_room_ids_as_hashes: false,
|
||||
strict_event_byte_limits_room_versions: true,
|
||||
msc4242_state_dags: false,
|
||||
msc4311_stripped_state: false,
|
||||
};
|
||||
|
||||
const ROOM_VERSION_HYDRA_V11: RoomVersion = RoomVersion {
|
||||
@@ -561,6 +591,7 @@ const ROOM_VERSION_HYDRA_V11: RoomVersion = RoomVersion {
|
||||
msc4291_room_ids_as_hashes: true, // Changed from v11
|
||||
strict_event_byte_limits_room_versions: true,
|
||||
msc4242_state_dags: false,
|
||||
msc4311_stripped_state: false,
|
||||
};
|
||||
|
||||
const ROOM_VERSION_V12: RoomVersion = RoomVersion {
|
||||
@@ -586,6 +617,7 @@ const ROOM_VERSION_V12: RoomVersion = RoomVersion {
|
||||
msc4291_room_ids_as_hashes: true, // Changed from v11
|
||||
strict_event_byte_limits_room_versions: true,
|
||||
msc4242_state_dags: false,
|
||||
msc4311_stripped_state: true,
|
||||
};
|
||||
|
||||
const ROOM_VERSION_MSC4242V12: RoomVersion = RoomVersion {
|
||||
@@ -611,6 +643,7 @@ const ROOM_VERSION_MSC4242V12: RoomVersion = RoomVersion {
|
||||
msc4291_room_ids_as_hashes: true,
|
||||
strict_event_byte_limits_room_versions: true,
|
||||
msc4242_state_dags: true,
|
||||
msc4311_stripped_state: true,
|
||||
};
|
||||
|
||||
/// Helper class for managing the known room versions, and providing dict-like
|
||||
|
||||
@@ -1052,3 +1052,12 @@ def parse_stripped_state_event(raw_stripped_event: Any) -> StrippedStateEvent |
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def serialize_stripped_state_event(stripped_event: StrippedStateEvent) -> JsonDict:
|
||||
return {
|
||||
"type": stripped_event.type,
|
||||
"state_key": stripped_event.state_key,
|
||||
"sender": stripped_event.sender,
|
||||
"content": stripped_event.content,
|
||||
}
|
||||
|
||||
@@ -1405,6 +1405,11 @@ class FederationClient(FederationBase):
|
||||
},
|
||||
)
|
||||
except HttpResponseException as e:
|
||||
# TODO: MSC4311: The 400 `M_MISSING_PARAM` error SHOULD be translated to a 5xx
|
||||
# error by the sending server over the Client-Server API. This is done
|
||||
# because there's nothing the client can materially do differently to make
|
||||
# the request succeed.
|
||||
|
||||
# If an error is received that is due to an unrecognised endpoint,
|
||||
# fallback to the v1 endpoint if the room uses old-style event IDs.
|
||||
# Otherwise, consider it a legitimate error and raise.
|
||||
@@ -1428,6 +1433,10 @@ class FederationClient(FederationBase):
|
||||
event_id=pdu.event_id,
|
||||
content=pdu.get_pdu_json(time_now),
|
||||
)
|
||||
# TODO: MSC4311: The 400 `M_MISSING_PARAM` error SHOULD be translated to a 5xx
|
||||
# error by the sending server over the Client-Server API. This is done
|
||||
# because there's nothing the client can materially do differently to make
|
||||
# the request succeed.
|
||||
return content
|
||||
|
||||
async def send_leave(self, destinations: Iterable[str], pdu: EventBase) -> None:
|
||||
|
||||
@@ -772,8 +772,22 @@ class FederationServer(FederationBase):
|
||||
return {"event": pdu.get_templated_pdu_json(), "room_version": room_version}
|
||||
|
||||
async def on_invite_request(
|
||||
self, origin: str, content: JsonDict, room_version_id: str
|
||||
self,
|
||||
*,
|
||||
origin: str,
|
||||
expected_room_id: str,
|
||||
expected_event_id: str,
|
||||
event_json: JsonDict,
|
||||
room_version_id: str,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Args:
|
||||
origin:
|
||||
expected_room_id: The room ID specified in the
|
||||
`/_matrix/federation/v1/invite/{roomId}/{eventId}` request that we expect to
|
||||
match in the actual event itself.
|
||||
"""
|
||||
|
||||
room_version = KNOWN_ROOM_VERSIONS.get(room_version_id)
|
||||
if not room_version:
|
||||
raise SynapseError(
|
||||
@@ -782,9 +796,21 @@ class FederationServer(FederationBase):
|
||||
Codes.UNSUPPORTED_ROOM_VERSION,
|
||||
)
|
||||
|
||||
pdu = event_from_pdu_json(content, room_version)
|
||||
pdu = event_from_pdu_json(event_json, room_version)
|
||||
origin_host, _ = parse_server_name(origin)
|
||||
await self.check_server_matches_acl(origin_host, pdu.room_id)
|
||||
if pdu.event_id != expected_event_id:
|
||||
raise SynapseError(
|
||||
400,
|
||||
Codes.INVALID_PARAM,
|
||||
"Invite event ID must match event ID specified in the federation `/invite` request",
|
||||
)
|
||||
if pdu.room_id != expected_room_id:
|
||||
raise SynapseError(
|
||||
400,
|
||||
Codes.INVALID_PARAM,
|
||||
"The room_id specified in the invite event must match room ID specified in the federation `/invite` request",
|
||||
)
|
||||
if await self._spam_checker_module_callbacks.should_drop_federated_event(pdu):
|
||||
logger.info(
|
||||
"Federated event contains spam, dropping %s",
|
||||
@@ -797,7 +823,11 @@ class FederationServer(FederationBase):
|
||||
errmsg = f"event id {pdu.event_id}: {e}"
|
||||
logger.warning("%s", errmsg)
|
||||
raise SynapseError(403, errmsg, Codes.FORBIDDEN)
|
||||
ret_pdu = await self.handler.on_invite_request(origin, pdu, room_version)
|
||||
ret_pdu = await self.handler.on_invite_request(
|
||||
origin=origin,
|
||||
event=pdu,
|
||||
room_version=room_version,
|
||||
)
|
||||
time_now = self._clock.time_msec()
|
||||
return {"event": ret_pdu.get_pdu_json(time_now)}
|
||||
|
||||
|
||||
@@ -490,7 +490,11 @@ class FederationV1InviteServlet(BaseFederationServerServlet):
|
||||
# state resolution algorithm, and we don't use that for processing
|
||||
# invites
|
||||
result = await self.handler.on_invite_request(
|
||||
origin, content, room_version_id=RoomVersions.V1.identifier
|
||||
origin=origin,
|
||||
expected_room_id=room_id,
|
||||
expected_event_id=event_id,
|
||||
event_json=content,
|
||||
room_version_id=RoomVersions.V1.identifier,
|
||||
)
|
||||
|
||||
# V1 federation API is defined to return a content of `[200, {...}]`
|
||||
@@ -512,9 +516,6 @@ class FederationV2InviteServlet(BaseFederationServerServlet):
|
||||
room_id: str,
|
||||
event_id: str,
|
||||
) -> tuple[int, JsonDict]:
|
||||
# TODO(paul): assert that room_id/event_id parsed from path actually
|
||||
# match those given in content
|
||||
|
||||
room_version = content["room_version"]
|
||||
event = content["event"]
|
||||
invite_room_state = content.get("invite_room_state", [])
|
||||
@@ -523,12 +524,15 @@ class FederationV2InviteServlet(BaseFederationServerServlet):
|
||||
invite_room_state = []
|
||||
|
||||
# Synapse expects invite_room_state to be in unsigned, as it is in v1
|
||||
# API
|
||||
|
||||
# API. We will sanitize this inside `on_invite_request(...)`
|
||||
event.setdefault("unsigned", {})["invite_room_state"] = invite_room_state
|
||||
|
||||
result = await self.handler.on_invite_request(
|
||||
origin, event, room_version_id=room_version
|
||||
origin=origin,
|
||||
expected_room_id=room_id,
|
||||
expected_event_id=event_id,
|
||||
event_json=event,
|
||||
room_version_id=room_version,
|
||||
)
|
||||
|
||||
# We only store invite_room_state for internal use, so remove it before
|
||||
|
||||
@@ -59,7 +59,15 @@ from synapse.crypto.event_signing import compute_event_signature
|
||||
from synapse.event_auth import validate_event_for_room_version
|
||||
from synapse.events import EventBase
|
||||
from synapse.events.snapshot import EventContext, UnpersistedEventContextBase
|
||||
from synapse.events.utils import (
|
||||
parse_stripped_state_event,
|
||||
serialize_stripped_state_event,
|
||||
)
|
||||
from synapse.events.validator import EventValidator
|
||||
from synapse.federation.federation_base import (
|
||||
InvalidEventSignatureError,
|
||||
event_from_pdu_json,
|
||||
)
|
||||
from synapse.federation.federation_client import InvalidResponseError
|
||||
from synapse.handlers.pagination import PURGE_PAGINATION_LOCK_NAME
|
||||
from synapse.http.servlet import assert_params_in_dict
|
||||
@@ -1053,7 +1061,11 @@ class FederationHandler:
|
||||
return event
|
||||
|
||||
async def on_invite_request(
|
||||
self, origin: str, event: EventBase, room_version: RoomVersion
|
||||
self,
|
||||
*,
|
||||
origin: str,
|
||||
event: EventBase,
|
||||
room_version: RoomVersion,
|
||||
) -> EventBase:
|
||||
"""We've got an invite event. Process and persist it. Sign it.
|
||||
|
||||
@@ -1126,6 +1138,98 @@ class FederationHandler:
|
||||
room_id=event.room_id, room_version=room_version
|
||||
)
|
||||
|
||||
# Validate `invite_room_state` according to MSC4311:
|
||||
# > If any of the events are not a PDU, not for the room ID specified, or fail
|
||||
# > signature checks, or the `m.room.create` event is missing, the receiving
|
||||
# > server MAY respond to invites with a `400 M_MISSING_PARAM` standard Matrix
|
||||
# > error (new to the endpoint). For invites to room version 12+ rooms, servers
|
||||
# > SHOULD rather than MAY respond to such requests with `400 M_MISSING_PARAM`.
|
||||
invite_room_state = event.unsigned.get("invite_room_state")
|
||||
if invite_room_state is not None and room_version.msc4311_stripped_state:
|
||||
try:
|
||||
# Scrutinize JSON values
|
||||
assert isinstance(invite_room_state, list), (
|
||||
"`invite_room_state` must be a list of PDU's"
|
||||
)
|
||||
includes_create_event = False
|
||||
for raw_stripped_event in invite_room_state:
|
||||
# Validate PDU
|
||||
try:
|
||||
pdu = event_from_pdu_json(raw_stripped_event, room_version)
|
||||
except Exception as exc:
|
||||
raise AssertionError(
|
||||
"Unable to parse one of the `invite_room_state` event's as a PDU"
|
||||
) from exc
|
||||
|
||||
if pdu.type == EventTypes.Create:
|
||||
includes_create_event = True
|
||||
|
||||
# Validate that it's from the same room
|
||||
assert pdu.room_id == event.room_id, (
|
||||
"PDU must be from the room ID specified in the `/invite` request"
|
||||
)
|
||||
# Validate signature/hashes
|
||||
try:
|
||||
pdu = await self.federation_client._check_sigs_and_hash(
|
||||
room_version, pdu
|
||||
)
|
||||
except InvalidEventSignatureError as exc:
|
||||
raise AssertionError(
|
||||
"PDU must pass signature/hash checks"
|
||||
) from exc
|
||||
|
||||
# Validate `m.room.create` event is included
|
||||
assert includes_create_event, (
|
||||
"`invite_room_state` must include `m.room.create` event"
|
||||
)
|
||||
except Exception as exc:
|
||||
# FIXME: Reject with 400 `M_MISSING_PARAM` after 2027-01-01. Given Synapse
|
||||
# claimed to support room version 12 but didn't adhere to this behavior until
|
||||
# 2026-05-04, we will only warn for now.
|
||||
logger.warning(
|
||||
"Continuing anyway but failed to validate `invite_room_state` on invite %s: %s",
|
||||
event,
|
||||
exc,
|
||||
)
|
||||
|
||||
# With MSC4311: `invite_room_state` over federation can use full PDUs so we need
|
||||
# to convert them into "stripped state events" so they don't end up being sent
|
||||
# down to the client.
|
||||
#
|
||||
# We do this separate from the validation above as sending full PDU's can happen
|
||||
# in any room version.
|
||||
if invite_room_state is not None:
|
||||
try:
|
||||
# Scrutinize JSON values
|
||||
assert isinstance(invite_room_state, list), (
|
||||
"`invite_room_state` must be a list"
|
||||
)
|
||||
|
||||
new_invite_room_state = []
|
||||
for raw_stripped_event in invite_room_state:
|
||||
# Parse and serialize to strip the events down to only the necessary fields
|
||||
parsed_stripped_event = parse_stripped_state_event(
|
||||
raw_stripped_event
|
||||
)
|
||||
if parsed_stripped_event is None:
|
||||
raise AssertionError("Unable to parse as stripped event")
|
||||
serialized_stripped_event = serialize_stripped_state_event(
|
||||
parsed_stripped_event
|
||||
)
|
||||
new_invite_room_state.append(serialized_stripped_event)
|
||||
|
||||
# Replace with our sanitized `invite_room_state`
|
||||
event.unsigned["invite_room_state"] = new_invite_room_state
|
||||
except AssertionError as exc:
|
||||
# We did our best to sanitize but ultimately failed. Leave it as-is for
|
||||
# the client to interpret. Another valid decision would be to strip it
|
||||
# from `unsigned` but this is more forwards compatible.
|
||||
logger.warning(
|
||||
"Continuing anyway but failed to sanitize `invite_room_state` on invite %s: %s",
|
||||
event,
|
||||
exc,
|
||||
)
|
||||
|
||||
event.internal_metadata.outlier = True
|
||||
event.internal_metadata.out_of_band_membership = True
|
||||
|
||||
|
||||
@@ -123,6 +123,16 @@ class RoomVersion:
|
||||
to the create event every time we insert an event would be prohibitively expensive.
|
||||
This is similar to how doubly-linked lists can potentially not refer to previous items correctly
|
||||
without verifying the list's integrity, but doing it on every insert is too expensive."""
|
||||
msc4311_stripped_state: bool
|
||||
"""
|
||||
Whether the `m.room.create` event is required in the
|
||||
`invite_state`/`knock_state` and `invite_room_state`/`knock_room_state` in the
|
||||
client and federation API's.
|
||||
///
|
||||
Also determines whether full PDU's are returned in the
|
||||
`invite_room_state`/`knock_room_state` in the federation API. The client API
|
||||
still uses stripped state.
|
||||
"""
|
||||
|
||||
class RoomVersions:
|
||||
V1: RoomVersion
|
||||
|
||||
Reference in New Issue
Block a user