Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit 3b88722

Browse files
authored
Do not allow cross-room relations, per MSC2674. (#11516)
1 parent 0cc3bf9 commit 3b88722

File tree

6 files changed

+161
-17
lines changed

6 files changed

+161
-17
lines changed

changelog.d/11516.bugfix

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix a long-standing bug where relations from other rooms could be included in the bundled aggregations of an event.

synapse/events/utils.py

+7-4
Original file line numberDiff line numberDiff line change
@@ -454,23 +454,26 @@ async def _injected_bundled_aggregations(
454454
return
455455

456456
event_id = event.event_id
457+
room_id = event.room_id
457458

458459
# The bundled aggregations to include.
459460
aggregations = {}
460461

461-
annotations = await self.store.get_aggregation_groups_for_event(event_id)
462+
annotations = await self.store.get_aggregation_groups_for_event(
463+
event_id, room_id
464+
)
462465
if annotations.chunk:
463466
aggregations[RelationTypes.ANNOTATION] = annotations.to_dict()
464467

465468
references = await self.store.get_relations_for_event(
466-
event_id, RelationTypes.REFERENCE, direction="f"
469+
event_id, room_id, RelationTypes.REFERENCE, direction="f"
467470
)
468471
if references.chunk:
469472
aggregations[RelationTypes.REFERENCE] = references.to_dict()
470473

471474
edit = None
472475
if event.type == EventTypes.Message:
473-
edit = await self.store.get_applicable_edit(event_id)
476+
edit = await self.store.get_applicable_edit(event_id, room_id)
474477

475478
if edit:
476479
# If there is an edit replace the content, preserving existing
@@ -503,7 +506,7 @@ async def _injected_bundled_aggregations(
503506
(
504507
thread_count,
505508
latest_thread_event,
506-
) = await self.store.get_thread_summary(event_id)
509+
) = await self.store.get_thread_summary(event_id, room_id)
507510
if latest_thread_event:
508511
aggregations[RelationTypes.THREAD] = {
509512
# Don't bundle aggregations as this could recurse forever.

synapse/rest/client/relations.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ async def on_GET(
212212

213213
pagination_chunk = await self.store.get_relations_for_event(
214214
event_id=parent_id,
215+
room_id=room_id,
215216
relation_type=relation_type,
216217
event_type=event_type,
217218
limit=limit,
@@ -317,6 +318,7 @@ async def on_GET(
317318

318319
pagination_chunk = await self.store.get_aggregation_groups_for_event(
319320
event_id=parent_id,
321+
room_id=room_id,
320322
event_type=event_type,
321323
limit=limit,
322324
from_token=from_token,
@@ -383,7 +385,9 @@ async def on_GET(
383385

384386
# This checks that a) the event exists and b) the user is allowed to
385387
# view it.
386-
await self.event_handler.get_event(requester.user, room_id, parent_id)
388+
event = await self.event_handler.get_event(requester.user, room_id, parent_id)
389+
if event is None:
390+
raise SynapseError(404, "Unknown parent event.")
387391

388392
if relation_type != RelationTypes.ANNOTATION:
389393
raise SynapseError(400, "Relation type must be 'annotation'")
@@ -402,6 +406,7 @@ async def on_GET(
402406

403407
result = await self.store.get_relations_for_event(
404408
event_id=parent_id,
409+
room_id=room_id,
405410
relation_type=relation_type,
406411
event_type=event_type,
407412
aggregation_key=key,

synapse/storage/databases/main/events.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -1780,10 +1780,14 @@ def _handle_event_relations(
17801780
)
17811781

17821782
if rel_type == RelationTypes.REPLACE:
1783-
txn.call_after(self.store.get_applicable_edit.invalidate, (parent_id,))
1783+
txn.call_after(
1784+
self.store.get_applicable_edit.invalidate, (parent_id, event.room_id)
1785+
)
17841786

17851787
if rel_type == RelationTypes.THREAD:
1786-
txn.call_after(self.store.get_thread_summary.invalidate, (parent_id,))
1788+
txn.call_after(
1789+
self.store.get_thread_summary.invalidate, (parent_id, event.room_id)
1790+
)
17871791

17881792
def _handle_insertion_event(self, txn: LoggingTransaction, event: EventBase):
17891793
"""Handles keeping track of insertion events and edges/connections.

synapse/storage/databases/main/relations.py

+26-10
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class RelationsWorkerStore(SQLBaseStore):
3737
async def get_relations_for_event(
3838
self,
3939
event_id: str,
40+
room_id: str,
4041
relation_type: Optional[str] = None,
4142
event_type: Optional[str] = None,
4243
aggregation_key: Optional[str] = None,
@@ -49,6 +50,7 @@ async def get_relations_for_event(
4950
5051
Args:
5152
event_id: Fetch events that relate to this event ID.
53+
room_id: The room the event belongs to.
5254
relation_type: Only fetch events with this relation type, if given.
5355
event_type: Only fetch events with this event type, if given.
5456
aggregation_key: Only fetch events with this aggregation key, if given.
@@ -63,8 +65,8 @@ async def get_relations_for_event(
6365
the form `{"event_id": "..."}`.
6466
"""
6567

66-
where_clause = ["relates_to_id = ?"]
67-
where_args: List[Union[str, int]] = [event_id]
68+
where_clause = ["relates_to_id = ?", "room_id = ?"]
69+
where_args: List[Union[str, int]] = [event_id, room_id]
6870

6971
if relation_type is not None:
7072
where_clause.append("relation_type = ?")
@@ -199,6 +201,7 @@ async def event_is_target_of_relation(self, parent_id: str) -> bool:
199201
async def get_aggregation_groups_for_event(
200202
self,
201203
event_id: str,
204+
room_id: str,
202205
event_type: Optional[str] = None,
203206
limit: int = 5,
204207
direction: str = "b",
@@ -213,6 +216,7 @@ async def get_aggregation_groups_for_event(
213216
214217
Args:
215218
event_id: Fetch events that relate to this event ID.
219+
room_id: The room the event belongs to.
216220
event_type: Only fetch events with this event type, if given.
217221
limit: Only fetch the `limit` groups.
218222
direction: Whether to fetch the highest count first (`"b"`) or
@@ -225,8 +229,12 @@ async def get_aggregation_groups_for_event(
225229
`type`, `key` and `count` fields.
226230
"""
227231

228-
where_clause = ["relates_to_id = ?", "relation_type = ?"]
229-
where_args: List[Union[str, int]] = [event_id, RelationTypes.ANNOTATION]
232+
where_clause = ["relates_to_id = ?", "room_id = ?", "relation_type = ?"]
233+
where_args: List[Union[str, int]] = [
234+
event_id,
235+
room_id,
236+
RelationTypes.ANNOTATION,
237+
]
230238

231239
if event_type:
232240
where_clause.append("type = ?")
@@ -288,14 +296,17 @@ def _get_aggregation_groups_for_event_txn(
288296
)
289297

290298
@cached()
291-
async def get_applicable_edit(self, event_id: str) -> Optional[EventBase]:
299+
async def get_applicable_edit(
300+
self, event_id: str, room_id: str
301+
) -> Optional[EventBase]:
292302
"""Get the most recent edit (if any) that has happened for the given
293303
event.
294304
295305
Correctly handles checking whether edits were allowed to happen.
296306
297307
Args:
298308
event_id: The original event ID
309+
room_id: The original event's room ID
299310
300311
Returns:
301312
The most recent edit, if any.
@@ -317,13 +328,14 @@ async def get_applicable_edit(self, event_id: str) -> Optional[EventBase]:
317328
WHERE
318329
relates_to_id = ?
319330
AND relation_type = ?
331+
AND edit.room_id = ?
320332
AND edit.type = 'm.room.message'
321333
ORDER by edit.origin_server_ts DESC, edit.event_id DESC
322334
LIMIT 1
323335
"""
324336

325337
def _get_applicable_edit_txn(txn: LoggingTransaction) -> Optional[str]:
326-
txn.execute(sql, (event_id, RelationTypes.REPLACE))
338+
txn.execute(sql, (event_id, RelationTypes.REPLACE, room_id))
327339
row = txn.fetchone()
328340
if row:
329341
return row[0]
@@ -340,13 +352,14 @@ def _get_applicable_edit_txn(txn: LoggingTransaction) -> Optional[str]:
340352

341353
@cached()
342354
async def get_thread_summary(
343-
self, event_id: str
355+
self, event_id: str, room_id: str
344356
) -> Tuple[int, Optional[EventBase]]:
345357
"""Get the number of threaded replies, the senders of those replies, and
346358
the latest reply (if any) for the given event.
347359
348360
Args:
349-
event_id: The original event ID
361+
event_id: Summarize the thread related to this event ID.
362+
room_id: The room the event belongs to.
350363
351364
Returns:
352365
The number of items in the thread and the most recent response, if any.
@@ -363,12 +376,13 @@ def _get_thread_summary_txn(
363376
INNER JOIN events USING (event_id)
364377
WHERE
365378
relates_to_id = ?
379+
AND room_id = ?
366380
AND relation_type = ?
367381
ORDER BY topological_ordering DESC, stream_ordering DESC
368382
LIMIT 1
369383
"""
370384

371-
txn.execute(sql, (event_id, RelationTypes.THREAD))
385+
txn.execute(sql, (event_id, room_id, RelationTypes.THREAD))
372386
row = txn.fetchone()
373387
if row is None:
374388
return 0, None
@@ -378,11 +392,13 @@ def _get_thread_summary_txn(
378392
sql = """
379393
SELECT COALESCE(COUNT(event_id), 0)
380394
FROM event_relations
395+
INNER JOIN events USING (event_id)
381396
WHERE
382397
relates_to_id = ?
398+
AND room_id = ?
383399
AND relation_type = ?
384400
"""
385-
txn.execute(sql, (event_id, RelationTypes.THREAD))
401+
txn.execute(sql, (event_id, room_id, RelationTypes.THREAD))
386402
count = txn.fetchone()[0] # type: ignore[index]
387403

388404
return count, latest_event_id

tests/rest/client/test_relations.py

+115
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,16 @@
1616
import itertools
1717
import urllib.parse
1818
from typing import Dict, List, Optional, Tuple
19+
from unittest.mock import patch
1920

2021
from synapse.api.constants import EventTypes, RelationTypes
2122
from synapse.rest import admin
2223
from synapse.rest.client import login, register, relations, room, sync
2324

2425
from tests import unittest
2526
from tests.server import FakeChannel
27+
from tests.test_utils import make_awaitable
28+
from tests.test_utils.event_injection import inject_event
2629

2730

2831
class RelationsTestCase(unittest.HomeserverTestCase):
@@ -651,6 +654,118 @@ def test_aggregation_get_event_for_thread(self):
651654
},
652655
)
653656

657+
@unittest.override_config({"experimental_features": {"msc3440_enabled": True}})
658+
def test_ignore_invalid_room(self):
659+
"""Test that we ignore invalid relations over federation."""
660+
# Create another room and send a message in it.
661+
room2 = self.helper.create_room_as(self.user_id, tok=self.user_token)
662+
res = self.helper.send(room2, body="Hi!", tok=self.user_token)
663+
parent_id = res["event_id"]
664+
665+
# Disable the validation to pretend this came over federation.
666+
with patch(
667+
"synapse.handlers.message.EventCreationHandler._validate_event_relation",
668+
new=lambda self, event: make_awaitable(None),
669+
):
670+
# Generate a various relations from a different room.
671+
self.get_success(
672+
inject_event(
673+
self.hs,
674+
room_id=self.room,
675+
type="m.reaction",
676+
sender=self.user_id,
677+
content={
678+
"m.relates_to": {
679+
"rel_type": RelationTypes.ANNOTATION,
680+
"event_id": parent_id,
681+
"key": "A",
682+
}
683+
},
684+
)
685+
)
686+
687+
self.get_success(
688+
inject_event(
689+
self.hs,
690+
room_id=self.room,
691+
type="m.room.message",
692+
sender=self.user_id,
693+
content={
694+
"body": "foo",
695+
"msgtype": "m.text",
696+
"m.relates_to": {
697+
"rel_type": RelationTypes.REFERENCE,
698+
"event_id": parent_id,
699+
},
700+
},
701+
)
702+
)
703+
704+
self.get_success(
705+
inject_event(
706+
self.hs,
707+
room_id=self.room,
708+
type="m.room.message",
709+
sender=self.user_id,
710+
content={
711+
"body": "foo",
712+
"msgtype": "m.text",
713+
"m.relates_to": {
714+
"rel_type": RelationTypes.THREAD,
715+
"event_id": parent_id,
716+
},
717+
},
718+
)
719+
)
720+
721+
self.get_success(
722+
inject_event(
723+
self.hs,
724+
room_id=self.room,
725+
type="m.room.message",
726+
sender=self.user_id,
727+
content={
728+
"body": "foo",
729+
"msgtype": "m.text",
730+
"new_content": {
731+
"body": "new content",
732+
"msgtype": "m.text",
733+
},
734+
"m.relates_to": {
735+
"rel_type": RelationTypes.REPLACE,
736+
"event_id": parent_id,
737+
},
738+
},
739+
)
740+
)
741+
742+
# They should be ignored when fetching relations.
743+
channel = self.make_request(
744+
"GET",
745+
f"/_matrix/client/unstable/rooms/{room2}/relations/{parent_id}",
746+
access_token=self.user_token,
747+
)
748+
self.assertEquals(200, channel.code, channel.json_body)
749+
self.assertEqual(channel.json_body["chunk"], [])
750+
751+
# And when fetching aggregations.
752+
channel = self.make_request(
753+
"GET",
754+
f"/_matrix/client/unstable/rooms/{room2}/aggregations/{parent_id}",
755+
access_token=self.user_token,
756+
)
757+
self.assertEquals(200, channel.code, channel.json_body)
758+
self.assertEqual(channel.json_body["chunk"], [])
759+
760+
# And for bundled aggregations.
761+
channel = self.make_request(
762+
"GET",
763+
f"/rooms/{room2}/event/{parent_id}",
764+
access_token=self.user_token,
765+
)
766+
self.assertEquals(200, channel.code, channel.json_body)
767+
self.assertNotIn("m.relations", channel.json_body["unsigned"])
768+
654769
def test_edit(self):
655770
"""Test that a simple edit works."""
656771

0 commit comments

Comments
 (0)