summary refs log tree commit diff
diff options
context:
space:
mode:
authorShay <hillerys@element.io>2024-03-19 10:52:53 -0700
committerGitHub <noreply@github.com>2024-03-19 17:52:53 +0000
commit8fb5b0f335b3dc54962aea102c71a7e449497487 (patch)
tree698b5e92f822198a51131108a15fe61ac1501358
parentBump pydantic from 2.6.0 to 2.6.4 (#17004) (diff)
downloadsynapse-8fb5b0f335b3dc54962aea102c71a7e449497487.tar.xz
Improve event validation (#16908)
As the title states.
-rw-r--r--changelog.d/16908.misc1
-rw-r--r--synapse/api/constants.py2
-rw-r--r--synapse/handlers/message.py13
-rw-r--r--synapse/handlers/sync.py12
-rw-r--r--tests/handlers/test_message.py40
-rw-r--r--tests/handlers/test_sync.py115
6 files changed, 180 insertions, 3 deletions
diff --git a/changelog.d/16908.misc b/changelog.d/16908.misc
new file mode 100644
index 0000000000..d13c59aa35
--- /dev/null
+++ b/changelog.d/16908.misc
@@ -0,0 +1 @@
+Improve event validation (#16908).
\ No newline at end of file
diff --git a/synapse/api/constants.py b/synapse/api/constants.py
index d25aff98ff..98884b4967 100644
--- a/synapse/api/constants.py
+++ b/synapse/api/constants.py
@@ -129,6 +129,8 @@ class EventTypes:
 
     Reaction: Final = "m.reaction"
 
+    CallInvite: Final = "m.call.invite"
+
 
 class ToDeviceEventTypes:
     RoomKeyRequest: Final = "m.room_key_request"
diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py
index 0ce6eeee15..ccaa5508ff 100644
--- a/synapse/handlers/message.py
+++ b/synapse/handlers/message.py
@@ -34,6 +34,7 @@ from synapse.api.constants import (
     EventTypes,
     GuestAccess,
     HistoryVisibility,
+    JoinRules,
     Membership,
     RelationTypes,
     UserTypes,
@@ -1325,6 +1326,18 @@ class EventCreationHandler:
 
         self.validator.validate_new(event, self.config)
         await self._validate_event_relation(event)
+
+        if event.type == EventTypes.CallInvite:
+            room_id = event.room_id
+            room_info = await self.store.get_room_with_stats(room_id)
+            assert room_info is not None
+
+            if room_info.join_rules == JoinRules.PUBLIC:
+                raise SynapseError(
+                    403,
+                    "Call invites are not allowed in public rooms.",
+                    Codes.FORBIDDEN,
+                )
         logger.debug("Created event %s", event.event_id)
 
         return event, context
diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py
index 0aedb37f16..3aa2e2b7ba 100644
--- a/synapse/handlers/sync.py
+++ b/synapse/handlers/sync.py
@@ -41,6 +41,7 @@ from synapse.api.constants import (
     AccountDataTypes,
     EventContentFields,
     EventTypes,
+    JoinRules,
     Membership,
 )
 from synapse.api.filtering import FilterCollection
@@ -675,13 +676,22 @@ class SyncHandler:
                         )
                     )
 
-                loaded_recents = await filter_events_for_client(
+                filtered_recents = await filter_events_for_client(
                     self._storage_controllers,
                     sync_config.user.to_string(),
                     loaded_recents,
                     always_include_ids=current_state_ids,
                 )
 
+                loaded_recents = []
+                for event in filtered_recents:
+                    if event.type == EventTypes.CallInvite:
+                        room_info = await self.store.get_room_with_stats(event.room_id)
+                        assert room_info is not None
+                        if room_info.join_rules == JoinRules.PUBLIC:
+                            continue
+                    loaded_recents.append(event)
+
                 log_kv({"loaded_recents_after_client_filtering": len(loaded_recents)})
 
                 loaded_recents.extend(recents)
diff --git a/tests/handlers/test_message.py b/tests/handlers/test_message.py
index 0ee5eee385..76ab83d1f7 100644
--- a/tests/handlers/test_message.py
+++ b/tests/handlers/test_message.py
@@ -24,6 +24,7 @@ from typing import Tuple
 from twisted.test.proto_helpers import MemoryReactor
 
 from synapse.api.constants import EventTypes
+from synapse.api.errors import SynapseError
 from synapse.events import EventBase
 from synapse.events.snapshot import EventContext, UnpersistedEventContextBase
 from synapse.rest import admin
@@ -51,11 +52,15 @@ class EventCreationTestCase(unittest.HomeserverTestCase):
         persistence = self.hs.get_storage_controllers().persistence
         assert persistence is not None
         self._persist_event_storage_controller = persistence
+        self.store = self.hs.get_datastores().main
 
         self.user_id = self.register_user("tester", "foobar")
         device_id = "dev-1"
         access_token = self.login("tester", "foobar", device_id=device_id)
         self.room_id = self.helper.create_room_as(self.user_id, tok=access_token)
+        self.private_room_id = self.helper.create_room_as(
+            self.user_id, tok=access_token, extra_content={"preset": "private_chat"}
+        )
 
         self.requester = create_requester(self.user_id, device_id=device_id)
 
@@ -285,6 +290,41 @@ class EventCreationTestCase(unittest.HomeserverTestCase):
             AssertionError,
         )
 
+    def test_call_invite_event_creation_fails_in_public_room(self) -> None:
+        # get prev_events for room
+        prev_events = self.get_success(
+            self.store.get_prev_events_for_room(self.room_id)
+        )
+
+        # the invite in a public room should fail
+        self.get_failure(
+            self.handler.create_event(
+                self.requester,
+                {
+                    "type": EventTypes.CallInvite,
+                    "room_id": self.room_id,
+                    "sender": self.requester.user.to_string(),
+                },
+                prev_event_ids=prev_events,
+                auth_event_ids=prev_events,
+            ),
+            SynapseError,
+        )
+
+        # but a call invite in a private room should succeed
+        self.get_success(
+            self.handler.create_event(
+                self.requester,
+                {
+                    "type": EventTypes.CallInvite,
+                    "room_id": self.private_room_id,
+                    "sender": self.requester.user.to_string(),
+                },
+                prev_event_ids=prev_events,
+                auth_event_ids=prev_events,
+            )
+        )
+
 
 class ServerAclValidationTestCase(unittest.HomeserverTestCase):
     servlets = [
diff --git a/tests/handlers/test_sync.py b/tests/handlers/test_sync.py
index 37904926e3..1b36324b8f 100644
--- a/tests/handlers/test_sync.py
+++ b/tests/handlers/test_sync.py
@@ -17,7 +17,7 @@
 # [This file includes modifications made by New Vector Limited]
 #
 #
-from typing import Optional
+from typing import Collection, List, Optional
 from unittest.mock import AsyncMock, Mock, patch
 
 from twisted.test.proto_helpers import MemoryReactor
@@ -25,7 +25,10 @@ from twisted.test.proto_helpers import MemoryReactor
 from synapse.api.constants import EventTypes, JoinRules
 from synapse.api.errors import Codes, ResourceLimitError
 from synapse.api.filtering import Filtering
-from synapse.api.room_versions import RoomVersions
+from synapse.api.room_versions import RoomVersion, RoomVersions
+from synapse.events import EventBase
+from synapse.events.snapshot import EventContext
+from synapse.federation.federation_base import event_from_pdu_json
 from synapse.handlers.sync import SyncConfig, SyncResult
 from synapse.rest import admin
 from synapse.rest.client import knock, login, room
@@ -285,6 +288,114 @@ class SyncTestCase(tests.unittest.HomeserverTestCase):
         )
         self.assertEqual(eve_initial_sync_after_join.joined, [])
 
+    def test_call_invite_in_public_room_not_returned(self) -> None:
+        user = self.register_user("alice", "password")
+        tok = self.login(user, "password")
+        room_id = self.helper.create_room_as(user, is_public=True, tok=tok)
+        self.handler = self.hs.get_federation_handler()
+        federation_event_handler = self.hs.get_federation_event_handler()
+
+        async def _check_event_auth(
+            origin: Optional[str], event: EventBase, context: EventContext
+        ) -> None:
+            pass
+
+        federation_event_handler._check_event_auth = _check_event_auth  # type: ignore[method-assign]
+        self.client = self.hs.get_federation_client()
+
+        async def _check_sigs_and_hash_for_pulled_events_and_fetch(
+            dest: str, pdus: Collection[EventBase], room_version: RoomVersion
+        ) -> List[EventBase]:
+            return list(pdus)
+
+        self.client._check_sigs_and_hash_for_pulled_events_and_fetch = _check_sigs_and_hash_for_pulled_events_and_fetch  # type: ignore[assignment]
+
+        prev_events = self.get_success(self.store.get_prev_events_for_room(room_id))
+
+        # create a call invite event
+        call_event = event_from_pdu_json(
+            {
+                "type": EventTypes.CallInvite,
+                "content": {},
+                "room_id": room_id,
+                "sender": user,
+                "depth": 32,
+                "prev_events": prev_events,
+                "auth_events": prev_events,
+                "origin_server_ts": self.clock.time_msec(),
+            },
+            RoomVersions.V10,
+        )
+
+        self.assertEqual(
+            self.get_success(
+                federation_event_handler.on_receive_pdu("test.serv", call_event)
+            ),
+            None,
+        )
+
+        # check that it is in DB
+        recent_event = self.get_success(self.store.get_prev_events_for_room(room_id))
+        self.assertIn(call_event.event_id, recent_event)
+
+        # but that it does not come down /sync in public room
+        sync_result: SyncResult = self.get_success(
+            self.sync_handler.wait_for_sync_for_user(
+                create_requester(user), generate_sync_config(user)
+            )
+        )
+        event_ids = []
+        for event in sync_result.joined[0].timeline.events:
+            event_ids.append(event.event_id)
+        self.assertNotIn(call_event.event_id, event_ids)
+
+        # it will come down in a private room, though
+        user2 = self.register_user("bob", "password")
+        tok2 = self.login(user2, "password")
+        private_room_id = self.helper.create_room_as(
+            user2, is_public=False, tok=tok2, extra_content={"preset": "private_chat"}
+        )
+
+        priv_prev_events = self.get_success(
+            self.store.get_prev_events_for_room(private_room_id)
+        )
+        private_call_event = event_from_pdu_json(
+            {
+                "type": EventTypes.CallInvite,
+                "content": {},
+                "room_id": private_room_id,
+                "sender": user,
+                "depth": 32,
+                "prev_events": priv_prev_events,
+                "auth_events": priv_prev_events,
+                "origin_server_ts": self.clock.time_msec(),
+            },
+            RoomVersions.V10,
+        )
+
+        self.assertEqual(
+            self.get_success(
+                federation_event_handler.on_receive_pdu("test.serv", private_call_event)
+            ),
+            None,
+        )
+
+        recent_events = self.get_success(
+            self.store.get_prev_events_for_room(private_room_id)
+        )
+        self.assertIn(private_call_event.event_id, recent_events)
+
+        private_sync_result: SyncResult = self.get_success(
+            self.sync_handler.wait_for_sync_for_user(
+                create_requester(user2), generate_sync_config(user2)
+            )
+        )
+        priv_event_ids = []
+        for event in private_sync_result.joined[0].timeline.events:
+            priv_event_ids.append(event.event_id)
+
+        self.assertIn(private_call_event.event_id, priv_event_ids)
+
 
 _request_key = 0