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

Improve validation of field size limits in events. #14664

Merged
merged 16 commits into from
Dec 13, 2022
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/14664.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Improve validation of field size limits in events.
1 change: 1 addition & 0 deletions synapse/api/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ class EduTypes:

class RejectedReason:
AUTH_ERROR: Final = "auth_error"
OVERSIZED_EVENT: Final = "oversized_event"


class RoomCreationPreset:
Expand Down
11 changes: 10 additions & 1 deletion synapse/api/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,8 +424,17 @@ def error_dict(self, config: Optional["HomeServerConfig"]) -> "JsonDict":
class EventSizeError(SynapseError):
"""An error raised when an event is too big."""

def __init__(self, msg: str):
def __init__(self, msg: str, unpersistable: bool):
"""
strict:
if True, the PDU must not be persisted, not even as a rejected PDU
when received over federation.
This is notably true when the entire PDU exceeds the size limit for a PDU,
(as opposed to an individual key's size limit being exceeded).
"""

super().__init__(413, msg, Codes.TOO_LARGE)
self.unpersistable = unpersistable


class LoginError(SynapseError):
Expand Down
5 changes: 4 additions & 1 deletion synapse/api/room_versions.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,10 @@ class PushRuleRoomFlag:
EXTENSIBLE_EVENTS = "org.matrix.msc3932.extensible_events"


@attr.s(slots=True, frozen=True, auto_attribs=True)
# eq=False means equality and hashing are done using object identity, which suits
# our purposes because each RoomVersion is instantiated only once and some of
# the fields are not hashable.
@attr.s(slots=True, frozen=True, auto_attribs=True, eq=False)
reivilibre marked this conversation as resolved.
Show resolved Hide resolved
class RoomVersion:
"""An object which describes the unique attributes of a room version."""

Expand Down
75 changes: 68 additions & 7 deletions synapse/event_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
KNOWN_ROOM_VERSIONS,
EventFormatVersions,
RoomVersion,
RoomVersions,
)
from synapse.storage.databases.main.events_worker import EventRedactBehaviour
from synapse.types import MutableStateMap, StateMap, UserID, get_domain_from_id
Expand Down Expand Up @@ -341,19 +342,79 @@ def check_state_dependent_auth_rules(
logger.debug("Allowing! %s", event)


# Set of room versions where Synapse did not apply event key size limits
# in bytes, but rather in codepoints.
# In these room versions, we are more lenient with event size validation.
LENIENT_EVENT_BYTE_LIMITS_ROOM_VERSIONS = {
RoomVersions.V1,
RoomVersions.V2,
RoomVersions.V3,
RoomVersions.V4,
RoomVersions.V5,
RoomVersions.V6,
RoomVersions.MSC2176,
RoomVersions.V7,
RoomVersions.V8,
RoomVersions.V9,
RoomVersions.MSC3787,
RoomVersions.V10,
RoomVersions.MSC2716v4,
}


def _check_size_limits(event: "EventBase") -> None:
"""
Checks the size limits in a PDU.

The entire size limit of the PDU is checked first.
Then the size of fields is checked, first in codepoints and then in bytes.

The codepoint size limits are only for Synapse compatibility.

Raises:
EventSizeError:
when a size limit has been violated.

strict=True if Synapse never would have accepted the event and
the PDU must NOT be persisted.

strict=False if a prior version of Synapse would have accepted the
event and so the PDU must be persisted as rejected to avoid
breaking the room.
"""

# Whole PDU check
if len(encode_canonical_json(event.get_pdu_json())) > MAX_PDU_SIZE:
raise EventSizeError("event too large", unpersistable=True)
reivilibre marked this conversation as resolved.
Show resolved Hide resolved

# Codepoint size check: Synapse always enforced these limits, so apply
# them strictly.
if len(event.user_id) > 255:
raise EventSizeError("'user_id' too large")
raise EventSizeError("'user_id' too large", unpersistable=True)
if len(event.room_id) > 255:
raise EventSizeError("'room_id' too large")
raise EventSizeError("'room_id' too large", unpersistable=True)
if event.is_state() and len(event.state_key) > 255:
raise EventSizeError("'state_key' too large")
raise EventSizeError("'state_key' too large", unpersistable=True)
if len(event.type) > 255:
raise EventSizeError("'type' too large")
raise EventSizeError("'type' too large", unpersistable=True)
if len(event.event_id) > 255:
raise EventSizeError("'event_id' too large")
if len(encode_canonical_json(event.get_pdu_json())) > MAX_PDU_SIZE:
raise EventSizeError("event too large")
raise EventSizeError("'event_id' too large", unpersistable=True)

strict_byte_limits = (
event.room_version not in LENIENT_EVENT_BYTE_LIMITS_ROOM_VERSIONS
)

# Byte size check: if these fail, then be lenient to avoid breaking rooms.
if len(event.user_id.encode("utf-8")) > 255:
raise EventSizeError("'user_id' too large", unpersistable=strict_byte_limits)
if len(event.room_id.encode("utf-8")) > 255:
raise EventSizeError("'room_id' too large", unpersistable=strict_byte_limits)
if event.is_state() and len(event.state_key.encode("utf-8")) > 255:
raise EventSizeError("'state_key' too large", unpersistable=strict_byte_limits)
if len(event.type.encode("utf-8")) > 255:
raise EventSizeError("'type' too large", unpersistable=strict_byte_limits)
if len(event.event_id.encode("utf-8")) > 255:
raise EventSizeError("'event_id' too large", unpersistable=strict_byte_limits)


def _check_create(event: "EventBase") -> None:
Expand Down
22 changes: 22 additions & 0 deletions synapse/handlers/federation_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
from synapse.api.errors import (
AuthError,
Codes,
EventSizeError,
FederationError,
FederationPullAttemptBackoffError,
HttpResponseException,
Expand Down Expand Up @@ -1736,6 +1737,16 @@ async def prep(event: EventBase) -> None:
except AuthError as e:
logger.warning("Rejecting %r because %s", event, e)
context.rejected = RejectedReason.AUTH_ERROR
except EventSizeError as e:
if e.unpersistable:
# A strict event size error means the event is completely
# unpersistable.
raise e
# Otherwise, for non-strict errors, we just persist the event
# as rejected, for moderate compatibility with older Synapse
# versions.
logger.warning("While validating received event %r: %s", event, e)
context.rejected = RejectedReason.OVERSIZED_EVENT

events_and_contexts_to_persist.append((event, context))

Expand Down Expand Up @@ -1781,6 +1792,17 @@ async def _check_event_auth(
# TODO: use a different rejected reason here?
context.rejected = RejectedReason.AUTH_ERROR
return
except EventSizeError as e:
if e.unpersistable:
# A strict event size error means the event is completely
# unpersistable.
raise e
# Otherwise, for non-strict errors, we just persist the event
# as rejected, for moderate compatibility with older Synapse
# versions.
logger.warning("While validating received event %r: %s", event, e)
context.rejected = RejectedReason.OVERSIZED_EVENT
return

# next, check that we have all of the event's auth events.
#
Expand Down