diff --git a/changelog.d/16908.misc b/changelog.d/16908.misc new file mode 100644 index 00000000000..d13c59aa35e --- /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 f3d2c8073da..d5bac686924 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -130,6 +130,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 7e5bb97f2a4..c5011c83eb9 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -34,6 +34,7 @@ EventTypes, GuestAccess, HistoryVisibility, + JoinRules, Membership, RelationTypes, UserTypes, @@ -1325,6 +1326,18 @@ async def create_new_client_event( 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 9122a79b4c0..93219efaeea 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -41,6 +41,7 @@ AccountDataTypes, EventContentFields, EventTypes, + JoinRules, Membership, ) from synapse.api.filtering import FilterCollection @@ -675,13 +676,22 @@ async def _load_filtered_recents( ) ) - 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 0ee5eee3857..76ab83d1f77 100644 --- a/tests/handlers/test_message.py +++ b/tests/handlers/test_message.py @@ -24,6 +24,7 @@ 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 @@ def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None: 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 @@ def test_when_empty_prev_events_allowed_reject_event_with_empty_prev_events_and_ 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 37904926e30..1b36324b8fc 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 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 @@ def test_ban_wins_race_with_join(self) -> None: ) 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