From f4d1ab0027968621c6e6f1c8cc8dfc3ab8cd4b59 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Thu, 12 Dec 2019 13:51:25 +0000 Subject: [PATCH] Add the ability to restrict max avatar filesize and content-type (#19) --- changelog.d/19.feature | 1 + docs/sample_config.yaml | 24 ++++++++++++++++ synapse/config/repository.py | 30 ++++++++++++++++++++ synapse/handlers/profile.py | 46 +++++++++++++++++++++++++++++++ synapse/rest/client/v1/profile.py | 5 ++-- 5 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 changelog.d/19.feature diff --git a/changelog.d/19.feature b/changelog.d/19.feature new file mode 100644 index 0000000000..95a44a4a89 --- /dev/null +++ b/changelog.d/19.feature @@ -0,0 +1 @@ +Add `max_avatar_size` and `allowed_avatar_mimetypes` to restrict the size of user avatars and their file type respectively. \ No newline at end of file diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 7531e3aef8..37c53e4f69 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -663,6 +663,30 @@ uploads_path: "DATADIR/uploads" # #max_upload_size: 10M +# The largest allowed size for a user avatar. If not defined, no +# restriction will be imposed. +# +# Note that this only applies when an avatar is changed globally. +# Per-room avatar changes are not affected. See allow_per_room_profiles +# for disabling that functionality. +# +# Note that user avatar changes will not work if this is set without +# using Synapse's local media repo. +# +#max_avatar_size: 10M + +# Allow mimetypes for a user avatar. If not defined, no restriction will +# be imposed. +# +# Note that this only applies when an avatar is changed globally. +# Per-room avatar changes are not affected. See allow_per_room_profiles +# for disabling that functionality. +# +# Note that user avatar changes will not work if this is set without +# using Synapse's local media repo. +# +#allowed_avatar_mimetypes: ["image/png", "image/jpeg", "image/gif"] + # Maximum number of pixels that will be thumbnailed # #max_image_pixels: 32M diff --git a/synapse/config/repository.py b/synapse/config/repository.py index fbfcecc240..2abede409a 100644 --- a/synapse/config/repository.py +++ b/synapse/config/repository.py @@ -111,6 +111,12 @@ def read_config(self, config): self.max_image_pixels = self.parse_size(config.get("max_image_pixels", "32M")) self.max_spider_size = self.parse_size(config.get("max_spider_size", "10M")) + self.max_avatar_size = config.get("max_avatar_size") + if self.max_avatar_size: + self.max_avatar_size = self.parse_size(self.max_avatar_size) + + self.allowed_avatar_mimetypes = config.get("allowed_avatar_mimetypes", []) + self.media_store_path = self.ensure_directory(config["media_store_path"]) backup_media_store_path = config.get("backup_media_store_path") @@ -247,6 +253,30 @@ def default_config(self, data_dir_path, **kwargs): # #max_upload_size: 10M + # The largest allowed size for a user avatar. If not defined, no + # restriction will be imposed. + # + # Note that this only applies when an avatar is changed globally. + # Per-room avatar changes are not affected. See allow_per_room_profiles + # for disabling that functionality. + # + # Note that user avatar changes will not work if this is set without + # using Synapse's local media repo. + # + #max_avatar_size: 10M + + # Allow mimetypes for a user avatar. If not defined, no restriction will + # be imposed. + # + # Note that this only applies when an avatar is changed globally. + # Per-room avatar changes are not affected. See allow_per_room_profiles + # for disabling that functionality. + # + # Note that user avatar changes will not work if this is set without + # using Synapse's local media repo. + # + #allowed_avatar_mimetypes: ["image/png", "image/jpeg", "image/gif"] + # Maximum number of pixels that will be thumbnailed # #max_image_pixels: 32M diff --git a/synapse/handlers/profile.py b/synapse/handlers/profile.py index 5e92c65492..584f804986 100644 --- a/synapse/handlers/profile.py +++ b/synapse/handlers/profile.py @@ -63,6 +63,9 @@ def __init__(self, hs): self.http_client = hs.get_simple_http_client() + self.max_avatar_size = hs.config.max_avatar_size + self.allowed_avatar_mimetypes = hs.config.allowed_avatar_mimetypes + if hs.config.worker_app is None: self.clock.looping_call( self._start_update_remote_profile_cache, self.PROFILE_UPDATE_MS, @@ -368,6 +371,35 @@ def set_avatar_url(self, target_user, requester, new_avatar_url, by_admin=False) 400, "Avatar URL is too long (max %i)" % (MAX_AVATAR_URL_LEN, ), ) + # Enforce a max avatar size if one is defined + if self.max_avatar_size or self.allowed_avatar_mimetypes: + media_id = self._validate_and_parse_media_id_from_avatar_url(new_avatar_url) + + # Check that this media exists locally + media_info = yield self.store.get_local_media(media_id) + if not media_info: + raise SynapseError( + 400, "Unknown media id supplied", errcode=Codes.NOT_FOUND + ) + + # Ensure avatar does not exceed max allowed avatar size + media_size = media_info["media_length"] + if self.max_avatar_size and media_size > self.max_avatar_size: + raise SynapseError( + 400, "Avatars must be less than %s bytes in size" % + (self.max_avatar_size,), errcode=Codes.TOO_LARGE, + ) + + # Ensure the avatar's file type is allowed + if ( + self.allowed_avatar_mimetypes + and media_info["media_type"] not in self.allowed_avatar_mimetypes + ): + raise SynapseError( + 400, "Avatar file type '%s' not allowed" % + media_info["media_type"], + ) + yield self.store.set_profile_avatar_url( target_user.localpart, new_avatar_url, new_batchnum, ) @@ -383,6 +415,20 @@ def set_avatar_url(self, target_user, requester, new_avatar_url, by_admin=False) # start a profile replication push run_in_background(self._replicate_profiles) + def _validate_and_parse_media_id_from_avatar_url(self, mxc): + """Validate and parse a provided avatar url and return the local media id + + Args: + mxc (str): A mxc URL + + Returns: + str: The ID of the media + """ + avatar_pieces = mxc.split("/") + if len(avatar_pieces) != 4 or avatar_pieces[0] != "mxc:": + raise SynapseError(400, "Invalid avatar URL '%s' supplied" % mxc) + return avatar_pieces[-1] + @defer.inlineCallbacks def on_profile_query(self, args): user = UserID.from_string(args["user_id"]) diff --git a/synapse/rest/client/v1/profile.py b/synapse/rest/client/v1/profile.py index 064bcddaeb..34361697df 100644 --- a/synapse/rest/client/v1/profile.py +++ b/synapse/rest/client/v1/profile.py @@ -134,12 +134,13 @@ def on_PUT(self, request, user_id): content = parse_json_object_from_request(request) try: - new_name = content["avatar_url"] + new_avatar_url = content["avatar_url"] except Exception: defer.returnValue((400, "Unable to parse name")) yield self.profile_handler.set_avatar_url( - user, requester, new_name, is_admin) + user, requester, new_avatar_url, is_admin + ) if self.hs.config.shadow_server: shadow_user = UserID(