summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--changelog.d/19.feature1
-rw-r--r--docs/sample_config.yaml24
-rw-r--r--synapse/config/repository.py30
-rw-r--r--synapse/handlers/profile.py46
-rw-r--r--synapse/rest/client/v1/profile.py5
5 files changed, 104 insertions, 2 deletions
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 @@ class ContentRepositoryConfig(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 @@ class ContentRepositoryConfig(Config):
         #
         #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 @@ class BaseProfileHandler(BaseHandler):
 
         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 @@ class BaseProfileHandler(BaseHandler):
                 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 @@ class BaseProfileHandler(BaseHandler):
         # 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 @@ class ProfileAvatarURLRestServlet(RestServlet):
 
         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(