From 8fd7148e6ab3c1346dbf6715bf60c50b0d3fa3b8 Mon Sep 17 00:00:00 2001
From: Shay <hillerys@element.io>
Date: Fri, 21 Feb 2025 02:06:44 -0800
Subject: [PATCH] Prevent suspended users from sending encrypted messages
 (#18157)

Missed in the first round.
---
 changelog.d/18157.bugfix        |   1 +
 synapse/handlers/message.py     |  32 ++++++++--
 tests/rest/client/test_rooms.py | 102 +++++++++++++++++++++++++++++++-
 3 files changed, 128 insertions(+), 7 deletions(-)
 create mode 100644 changelog.d/18157.bugfix

diff --git a/changelog.d/18157.bugfix b/changelog.d/18157.bugfix
new file mode 100644
index 0000000000..307e9c96ff
--- /dev/null
+++ b/changelog.d/18157.bugfix
@@ -0,0 +1 @@
+Prevent suspended users from sending encrypted messages.
diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py
index df3010ecf6..4642b8b578 100644
--- a/synapse/handlers/message.py
+++ b/synapse/handlers/message.py
@@ -644,11 +644,33 @@ class EventCreationHandler:
         """
         await self.auth_blocking.check_auth_blocking(requester=requester)
 
-        if event_dict["type"] == EventTypes.Message:
-            requester_suspended = await self.store.get_user_suspended_status(
-                requester.user.to_string()
-            )
-            if requester_suspended:
+        requester_suspended = await self.store.get_user_suspended_status(
+            requester.user.to_string()
+        )
+        if requester_suspended:
+            # We want to allow suspended users to perform "corrective" actions
+            # asked of them by server admins, such as redact their messages and
+            # leave rooms.
+            if event_dict["type"] in ["m.room.redaction", "m.room.member"]:
+                if event_dict["type"] == "m.room.redaction":
+                    event = await self.store.get_event(
+                        event_dict["content"]["redacts"], allow_none=True
+                    )
+                    if event:
+                        if event.sender != requester.user.to_string():
+                            raise SynapseError(
+                                403,
+                                "You can only redact your own events while account is suspended.",
+                                Codes.USER_ACCOUNT_SUSPENDED,
+                            )
+                if event_dict["type"] == "m.room.member":
+                    if event_dict["content"]["membership"] != "leave":
+                        raise SynapseError(
+                            403,
+                            "Changing membership while account is suspended is not allowed.",
+                            Codes.USER_ACCOUNT_SUSPENDED,
+                        )
+            else:
                 raise SynapseError(
                     403,
                     "Sending messages while account is suspended is not allowed.",
diff --git a/tests/rest/client/test_rooms.py b/tests/rest/client/test_rooms.py
index 604b585150..f6ec8a8b7b 100644
--- a/tests/rest/client/test_rooms.py
+++ b/tests/rest/client/test_rooms.py
@@ -1371,6 +1371,23 @@ class RoomJoinTestCase(RoomBase):
         )
         self.assertEqual(channel.json_body["errcode"], "M_USER_SUSPENDED")
 
+    def test_suspended_user_can_leave_room(self) -> None:
+        channel = self.make_request(
+            "POST", f"/join/{self.room1}", access_token=self.tok1
+        )
+        self.assertEqual(channel.code, 200)
+
+        # set the user as suspended
+        self.get_success(self.store.set_user_suspended_status(self.user1, True))
+
+        # leave room
+        channel = self.make_request(
+            "POST",
+            f"/rooms/{self.room1}/leave",
+            access_token=self.tok1,
+        )
+        self.assertEqual(channel.code, 200)
+
 
 class RoomAppserviceTsParamTestCase(unittest.HomeserverTestCase):
     servlets = [
@@ -3989,10 +4006,25 @@ class UserSuspensionTests(unittest.HomeserverTestCase):
         self.user2 = self.register_user("teresa", "hackme")
         self.tok2 = self.login("teresa", "hackme")
 
-        self.room1 = self.helper.create_room_as(room_creator=self.user1, tok=self.tok1)
+        self.admin = self.register_user("admin", "pass", True)
+        self.admin_tok = self.login("admin", "pass")
+
+        self.room1 = self.helper.create_room_as(
+            room_creator=self.user1, tok=self.tok1, room_version="11"
+        )
         self.store = hs.get_datastores().main
 
-    def test_suspended_user_cannot_send_message_to_room(self) -> None:
+        self.room2 = self.helper.create_room_as(
+            room_creator=self.user1, is_public=False, tok=self.tok1
+        )
+        self.helper.send_state(
+            self.room2,
+            EventTypes.RoomEncryption,
+            {EventContentFields.ENCRYPTION_ALGORITHM: "m.megolm.v1.aes-sha2"},
+            tok=self.tok1,
+        )
+
+    def test_suspended_user_cannot_send_message_to_public_room(self) -> None:
         # set the user as suspended
         self.get_success(self.store.set_user_suspended_status(self.user1, True))
 
@@ -4004,6 +4036,24 @@ class UserSuspensionTests(unittest.HomeserverTestCase):
         )
         self.assertEqual(channel.json_body["errcode"], "M_USER_SUSPENDED")
 
+    def test_suspended_user_cannot_send_message_to_encrypted_room(self) -> None:
+        channel = self.make_request(
+            "PUT",
+            f"/_synapse/admin/v1/suspend/{self.user1}",
+            {"suspend": True},
+            access_token=self.admin_tok,
+        )
+        self.assertEqual(channel.code, 200)
+        self.assertEqual(channel.json_body, {f"user_{self.user1}_suspended": True})
+
+        channel = self.make_request(
+            "PUT",
+            f"/rooms/{self.room2}/send/m.room.encrypted/1",
+            access_token=self.tok1,
+            content={},
+        )
+        self.assertEqual(channel.json_body["errcode"], "M_USER_SUSPENDED")
+
     def test_suspended_user_cannot_change_profile_data(self) -> None:
         # set the user as suspended
         self.get_success(self.store.set_user_suspended_status(self.user1, True))
@@ -4069,3 +4119,51 @@ class UserSuspensionTests(unittest.HomeserverTestCase):
             shorthand=False,
         )
         self.assertEqual(channel.code, 200)
+
+        channel = self.make_request(
+            "PUT",
+            f"/_matrix/client/v3/rooms/{self.room1}/send/m.room.redaction/3456346",
+            access_token=self.tok1,
+            content={"reason": "bogus", "redacts": event_id},
+            shorthand=False,
+        )
+        self.assertEqual(channel.json_body["errcode"], "M_USER_SUSPENDED")
+
+        channel = self.make_request(
+            "PUT",
+            f"/_matrix/client/v3/rooms/{self.room1}/send/m.room.redaction/3456346",
+            access_token=self.tok1,
+            content={"reason": "bogus", "redacts": event_id2},
+            shorthand=False,
+        )
+        self.assertEqual(channel.code, 200)
+
+    def test_suspended_user_cannot_ban_others(self) -> None:
+        # user to ban joins room user1 created
+        self.make_request("POST", f"/rooms/{self.room1}/join", access_token=self.tok2)
+
+        # suspend user1
+        self.get_success(self.store.set_user_suspended_status(self.user1, True))
+
+        # user1 tries to ban other user while suspended
+        channel = self.make_request(
+            "POST",
+            f"/_matrix/client/v3/rooms/{self.room1}/ban",
+            access_token=self.tok1,
+            content={"reason": "spite", "user_id": self.user2},
+            shorthand=False,
+        )
+        self.assertEqual(channel.json_body["errcode"], "M_USER_SUSPENDED")
+
+        # un-suspend user1
+        self.get_success(self.store.set_user_suspended_status(self.user1, False))
+
+        # ban now goes through
+        channel = self.make_request(
+            "POST",
+            f"/_matrix/client/v3/rooms/{self.room1}/ban",
+            access_token=self.tok1,
+            content={"reason": "spite", "user_id": self.user2},
+            shorthand=False,
+        )
+        self.assertEqual(channel.code, 200)
-- 
GitLab