diff --git a/synapse/federation/transport/server/federation.py b/synapse/federation/transport/server/federation.py
index a05e5d5319..093ba30d31 100644
--- a/synapse/federation/transport/server/federation.py
+++ b/synapse/federation/transport/server/federation.py
@@ -509,6 +509,9 @@ class FederationV2InviteServlet(BaseFederationServerServlet):
event = content["event"]
invite_room_state = content.get("invite_room_state", [])
+ if not isinstance(invite_room_state, list):
+ invite_room_state = []
+
# Synapse expects invite_room_state to be in unsigned, as it is in v1
# API
diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py
index 2b7aad5b58..17dd4af13e 100644
--- a/synapse/handlers/federation.py
+++ b/synapse/handlers/federation.py
@@ -880,6 +880,9 @@ class FederationHandler:
if stripped_room_state is None:
raise KeyError("Missing 'knock_room_state' field in send_knock response")
+ if not isinstance(stripped_room_state, list):
+ raise TypeError("'knock_room_state' has wrong type")
+
event.unsigned["knock_room_state"] = stripped_room_state
context = EventContext.for_outlier(self._storage_controllers)
diff --git a/synapse/handlers/sliding_sync/__init__.py b/synapse/handlers/sliding_sync/__init__.py
index 4f4faef524..459d3c3e24 100644
--- a/synapse/handlers/sliding_sync/__init__.py
+++ b/synapse/handlers/sliding_sync/__init__.py
@@ -39,6 +39,7 @@ from synapse.logging.opentracing import (
trace,
)
from synapse.storage.databases.main.roommember import extract_heroes_from_room_summary
+from synapse.storage.databases.main.state_deltas import StateDelta
from synapse.storage.databases.main.stream import PaginateFunction
from synapse.storage.roommember import (
MemberSummary,
@@ -48,6 +49,7 @@ from synapse.types import (
MutableStateMap,
PersistedEventPosition,
Requester,
+ RoomStreamToken,
SlidingSyncStreamToken,
StateMap,
StrCollection,
@@ -471,6 +473,64 @@ class SlidingSyncHandler:
return state_map
@trace
+ async def get_current_state_deltas_for_room(
+ self,
+ room_id: str,
+ room_membership_for_user_at_to_token: RoomsForUserType,
+ from_token: RoomStreamToken,
+ to_token: RoomStreamToken,
+ ) -> List[StateDelta]:
+ """
+ Get the state deltas between two tokens taking into account the user's
+ membership. If the user is LEAVE/BAN, we will only get the state deltas up to
+ their LEAVE/BAN event (inclusive).
+
+ (> `from_token` and <= `to_token`)
+ """
+ membership = room_membership_for_user_at_to_token.membership
+ # We don't know how to handle `membership` values other than these. The
+ # code below would need to be updated.
+ assert membership in (
+ Membership.JOIN,
+ Membership.INVITE,
+ Membership.KNOCK,
+ Membership.LEAVE,
+ Membership.BAN,
+ )
+
+ # People shouldn't see past their leave/ban event
+ if membership in (
+ Membership.LEAVE,
+ Membership.BAN,
+ ):
+ to_bound = (
+ room_membership_for_user_at_to_token.event_pos.to_room_stream_token()
+ )
+ # If we are participating in the room, we can get the latest current state in
+ # the room
+ elif membership == Membership.JOIN:
+ to_bound = to_token
+ # We can only rely on the stripped state included in the invite/knock event
+ # itself so there will never be any state deltas to send down.
+ elif membership in (Membership.INVITE, Membership.KNOCK):
+ return []
+ else:
+ # We don't know how to handle this type of membership yet
+ #
+ # FIXME: We should use `assert_never` here but for some reason
+ # the exhaustive matching doesn't recognize the `Never` here.
+ # assert_never(membership)
+ raise AssertionError(
+ f"Unexpected membership {membership} that we don't know how to handle yet"
+ )
+
+ return await self.store.get_current_state_deltas_for_room(
+ room_id=room_id,
+ from_token=from_token,
+ to_token=to_bound,
+ )
+
+ @trace
async def get_room_sync_data(
self,
sync_config: SlidingSyncConfig,
@@ -755,13 +815,19 @@ class SlidingSyncHandler:
stripped_state = []
if invite_or_knock_event.membership == Membership.INVITE:
- stripped_state.extend(
- invite_or_knock_event.unsigned.get("invite_room_state", [])
+ invite_state = invite_or_knock_event.unsigned.get(
+ "invite_room_state", []
)
+ if not isinstance(invite_state, list):
+ invite_state = []
+
+ stripped_state.extend(invite_state)
elif invite_or_knock_event.membership == Membership.KNOCK:
- stripped_state.extend(
- invite_or_knock_event.unsigned.get("knock_room_state", [])
- )
+ knock_state = invite_or_knock_event.unsigned.get("knock_room_state", [])
+ if not isinstance(knock_state, list):
+ knock_state = []
+
+ stripped_state.extend(knock_state)
stripped_state.append(strip_event(invite_or_knock_event))
@@ -790,8 +856,9 @@ class SlidingSyncHandler:
# TODO: Limit the number of state events we're about to send down
# the room, if its too many we should change this to an
# `initial=True`?
- deltas = await self.store.get_current_state_deltas_for_room(
+ deltas = await self.get_current_state_deltas_for_room(
room_id=room_id,
+ room_membership_for_user_at_to_token=room_membership_for_user_at_to_token,
from_token=from_bound,
to_token=to_token.room_key,
)
diff --git a/synapse/http/site.py b/synapse/http/site.py
index 1cd90cb9b7..e83a4447b2 100644
--- a/synapse/http/site.py
+++ b/synapse/http/site.py
@@ -21,6 +21,7 @@
import contextlib
import logging
import time
+from http import HTTPStatus
from typing import TYPE_CHECKING, Any, Generator, Optional, Tuple, Union
import attr
@@ -139,6 +140,41 @@ class SynapseRequest(Request):
self.synapse_site.site_tag,
)
+ # Twisted machinery: this method is called by the Channel once the full request has
+ # been received, to dispatch the request to a resource.
+ #
+ # We're patching Twisted to bail/abort early when we see someone trying to upload
+ # `multipart/form-data` so we can avoid Twisted parsing the entire request body into
+ # in-memory (specific problem of this specific `Content-Type`). This protects us
+ # from an attacker uploading something bigger than the available RAM and crashing
+ # the server with a `MemoryError`, or carefully block just enough resources to cause
+ # all other requests to fail.
+ #
+ # FIXME: This can be removed once we Twisted releases a fix and we update to a
+ # version that is patched
+ def requestReceived(self, command: bytes, path: bytes, version: bytes) -> None:
+ if command == b"POST":
+ ctype = self.requestHeaders.getRawHeaders(b"content-type")
+ if ctype and b"multipart/form-data" in ctype[0]:
+ self.method, self.uri = command, path
+ self.clientproto = version
+ self.code = HTTPStatus.UNSUPPORTED_MEDIA_TYPE.value
+ self.code_message = bytes(
+ HTTPStatus.UNSUPPORTED_MEDIA_TYPE.phrase, "ascii"
+ )
+ self.responseHeaders.setRawHeaders(b"content-length", [b"0"])
+
+ logger.warning(
+ "Aborting connection from %s because `content-type: multipart/form-data` is unsupported: %s %s",
+ self.client,
+ command,
+ path,
+ )
+ self.write(b"")
+ self.loseConnection()
+ return
+ return super().requestReceived(command, path, version)
+
def handleContentChunk(self, data: bytes) -> None:
# we should have a `content` by now.
assert self.content, "handleContentChunk() called before gotLength()"
diff --git a/synapse/media/thumbnailer.py b/synapse/media/thumbnailer.py
index 3845067835..d6b8ce4a09 100644
--- a/synapse/media/thumbnailer.py
+++ b/synapse/media/thumbnailer.py
@@ -67,6 +67,11 @@ class ThumbnailError(Exception):
class Thumbnailer:
FORMATS = {"image/jpeg": "JPEG", "image/png": "PNG"}
+ # Which image formats we allow Pillow to open.
+ # This should intentionally be kept restrictive, because the decoder of any
+ # format in this list becomes part of our trusted computing base.
+ PILLOW_FORMATS = ("jpeg", "png", "webp", "gif")
+
@staticmethod
def set_limits(max_image_pixels: int) -> None:
Image.MAX_IMAGE_PIXELS = max_image_pixels
@@ -76,7 +81,7 @@ class Thumbnailer:
self._closed = False
try:
- self.image = Image.open(input_path)
+ self.image = Image.open(input_path, formats=self.PILLOW_FORMATS)
except OSError as e:
# If an error occurs opening the image, a thumbnail won't be able to
# be generated.
diff --git a/synapse/push/push_tools.py b/synapse/push/push_tools.py
index 1ef881f702..3f3e4a9234 100644
--- a/synapse/push/push_tools.py
+++ b/synapse/push/push_tools.py
@@ -74,9 +74,13 @@ async def get_context_for_event(
room_state = []
if ev.content.get("membership") == Membership.INVITE:
- room_state = ev.unsigned.get("invite_room_state", [])
+ invite_room_state = ev.unsigned.get("invite_room_state", [])
+ if isinstance(invite_room_state, list):
+ room_state = invite_room_state
elif ev.content.get("membership") == Membership.KNOCK:
- room_state = ev.unsigned.get("knock_room_state", [])
+ knock_room_state = ev.unsigned.get("knock_room_state", [])
+ if isinstance(knock_room_state, list):
+ room_state = knock_room_state
# Ideally we'd reuse the logic in `calculate_room_name`, but that gets
# complicated to handle partial events vs pulling events from the DB.
diff --git a/synapse/rest/client/sync.py b/synapse/rest/client/sync.py
index 5c62a74f41..f4ef84a038 100644
--- a/synapse/rest/client/sync.py
+++ b/synapse/rest/client/sync.py
@@ -436,7 +436,12 @@ class SyncRestServlet(RestServlet):
)
unsigned = dict(invite.get("unsigned", {}))
invite["unsigned"] = unsigned
- invited_state = list(unsigned.pop("invite_room_state", []))
+
+ invited_state = unsigned.pop("invite_room_state", [])
+ if not isinstance(invited_state, list):
+ invited_state = []
+
+ invited_state = list(invited_state)
invited_state.append(invite)
invited[room.room_id] = {"invite_state": {"events": invited_state}}
@@ -476,7 +481,10 @@ class SyncRestServlet(RestServlet):
# Extract the stripped room state from the unsigned dict
# This is for clients to get a little bit of information about
# the room they've knocked on, without revealing any sensitive information
- knocked_state = list(unsigned.pop("knock_room_state", []))
+ knocked_state = unsigned.pop("knock_room_state", [])
+ if not isinstance(knocked_state, list):
+ knocked_state = []
+ knocked_state = list(knocked_state)
# Append the actual knock membership event itself as well. This provides
# the client with:
diff --git a/synapse/storage/databases/main/state_deltas.py b/synapse/storage/databases/main/state_deltas.py
index 117ee89d0a..b90f667da8 100644
--- a/synapse/storage/databases/main/state_deltas.py
+++ b/synapse/storage/databases/main/state_deltas.py
@@ -243,6 +243,13 @@ class StateDeltasStore(SQLBaseStore):
(> `from_token` and <= `to_token`)
"""
+ # We can bail early if the `from_token` is after the `to_token`
+ if (
+ to_token is not None
+ and from_token is not None
+ and to_token.is_before_or_eq(from_token)
+ ):
+ return []
if (
from_token is not None
|