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

Commit 4bd54b2

Browse files
authored
Do not allow MSC3440 threads to fork threads (#11161)
Adds validation to the Client-Server API to ensure that the potential thread head does not relate to another event already. This results in not allowing a thread to "fork" into other threads. If the target event is unknown for some reason (maybe it isn't visible to your homeserver), but is the target of other events it is assumed that the thread can be created from it. Otherwise, it is rejected as an unknown event.
1 parent e2dabec commit 4bd54b2

File tree

4 files changed

+176
-8
lines changed

4 files changed

+176
-8
lines changed

changelog.d/11161.feature

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Experimental support for the thread relation defined in [MSC3440](https://github.com/matrix-org/matrix-doc/pull/3440).

synapse/handlers/message.py

+48-6
Original file line numberDiff line numberDiff line change
@@ -1001,13 +1001,52 @@ async def create_new_client_event(
10011001
)
10021002

10031003
self.validator.validate_new(event, self.config)
1004+
await self._validate_event_relation(event)
1005+
logger.debug("Created event %s", event.event_id)
1006+
1007+
return event, context
1008+
1009+
async def _validate_event_relation(self, event: EventBase) -> None:
1010+
"""
1011+
Ensure the relation data on a new event is not bogus.
1012+
1013+
Args:
1014+
event: The event being created.
1015+
1016+
Raises:
1017+
SynapseError if the event is invalid.
1018+
"""
1019+
1020+
relation = event.content.get("m.relates_to")
1021+
if not relation:
1022+
return
1023+
1024+
relation_type = relation.get("rel_type")
1025+
if not relation_type:
1026+
return
1027+
1028+
# Ensure the parent is real.
1029+
relates_to = relation.get("event_id")
1030+
if not relates_to:
1031+
return
1032+
1033+
parent_event = await self.store.get_event(relates_to, allow_none=True)
1034+
if parent_event:
1035+
# And in the same room.
1036+
if parent_event.room_id != event.room_id:
1037+
raise SynapseError(400, "Relations must be in the same room")
1038+
1039+
else:
1040+
# There must be some reason that the client knows the event exists,
1041+
# see if there are existing relations. If so, assume everything is fine.
1042+
if not await self.store.event_is_target_of_relation(relates_to):
1043+
# Otherwise, the client can't know about the parent event!
1044+
raise SynapseError(400, "Can't send relation to unknown event")
10041045

10051046
# If this event is an annotation then we check that that the sender
10061047
# can't annotate the same way twice (e.g. stops users from liking an
10071048
# event multiple times).
1008-
relation = event.content.get("m.relates_to", {})
1009-
if relation.get("rel_type") == RelationTypes.ANNOTATION:
1010-
relates_to = relation["event_id"]
1049+
if relation_type == RelationTypes.ANNOTATION:
10111050
aggregation_key = relation["key"]
10121051

10131052
already_exists = await self.store.has_user_annotated_event(
@@ -1016,9 +1055,12 @@ async def create_new_client_event(
10161055
if already_exists:
10171056
raise SynapseError(400, "Can't send same reaction twice")
10181057

1019-
logger.debug("Created event %s", event.event_id)
1020-
1021-
return event, context
1058+
# Don't attempt to start a thread if the parent event is a relation.
1059+
elif relation_type == RelationTypes.THREAD:
1060+
if await self.store.event_includes_relation(relates_to):
1061+
raise SynapseError(
1062+
400, "Cannot start threads from an event with a relation"
1063+
)
10221064

10231065
@measure_func("handle_new_client_event")
10241066
async def handle_new_client_event(

synapse/storage/databases/main/relations.py

+65-2
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,69 @@ def _get_recent_references_for_event_txn(
132132
"get_recent_references_for_event", _get_recent_references_for_event_txn
133133
)
134134

135+
async def event_includes_relation(self, event_id: str) -> bool:
136+
"""Check if the given event relates to another event.
137+
138+
An event has a relation if it has a valid m.relates_to with a rel_type
139+
and event_id in the content:
140+
141+
{
142+
"content": {
143+
"m.relates_to": {
144+
"rel_type": "m.replace",
145+
"event_id": "$other_event_id"
146+
}
147+
}
148+
}
149+
150+
Args:
151+
event_id: The event to check.
152+
153+
Returns:
154+
True if the event includes a valid relation.
155+
"""
156+
157+
result = await self.db_pool.simple_select_one_onecol(
158+
table="event_relations",
159+
keyvalues={"event_id": event_id},
160+
retcol="event_id",
161+
allow_none=True,
162+
desc="event_includes_relation",
163+
)
164+
return result is not None
165+
166+
async def event_is_target_of_relation(self, parent_id: str) -> bool:
167+
"""Check if the given event is the target of another event's relation.
168+
169+
An event is the target of an event relation if it has a valid
170+
m.relates_to with a rel_type and event_id pointing to parent_id in the
171+
content:
172+
173+
{
174+
"content": {
175+
"m.relates_to": {
176+
"rel_type": "m.replace",
177+
"event_id": "$parent_id"
178+
}
179+
}
180+
}
181+
182+
Args:
183+
parent_id: The event to check.
184+
185+
Returns:
186+
True if the event is the target of another event's relation.
187+
"""
188+
189+
result = await self.db_pool.simple_select_one_onecol(
190+
table="event_relations",
191+
keyvalues={"relates_to_id": parent_id},
192+
retcol="event_id",
193+
allow_none=True,
194+
desc="event_is_target_of_relation",
195+
)
196+
return result is not None
197+
135198
@cached(tree=True)
136199
async def get_aggregation_groups_for_event(
137200
self,
@@ -362,7 +425,7 @@ async def events_have_relations(
362425
%s;
363426
"""
364427

365-
def _get_if_event_has_relations(txn) -> List[str]:
428+
def _get_if_events_have_relations(txn) -> List[str]:
366429
clauses: List[str] = []
367430
clause, args = make_in_list_sql_clause(
368431
txn.database_engine, "relates_to_id", parent_ids
@@ -387,7 +450,7 @@ def _get_if_event_has_relations(txn) -> List[str]:
387450
return [row[0] for row in txn]
388451

389452
return await self.db_pool.runInteraction(
390-
"get_if_event_has_relations", _get_if_event_has_relations
453+
"get_if_events_have_relations", _get_if_events_have_relations
391454
)
392455

393456
async def has_user_annotated_event(

tests/rest/client/test_relations.py

+62
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,49 @@ def test_deny_membership(self):
9191
channel = self._send_relation(RelationTypes.ANNOTATION, EventTypes.Member)
9292
self.assertEquals(400, channel.code, channel.json_body)
9393

94+
def test_deny_invalid_event(self):
95+
"""Test that we deny relations on non-existant events"""
96+
channel = self._send_relation(
97+
RelationTypes.ANNOTATION,
98+
EventTypes.Message,
99+
parent_id="foo",
100+
content={"body": "foo", "msgtype": "m.text"},
101+
)
102+
self.assertEquals(400, channel.code, channel.json_body)
103+
104+
# Unless that event is referenced from another event!
105+
self.get_success(
106+
self.hs.get_datastore().db_pool.simple_insert(
107+
table="event_relations",
108+
values={
109+
"event_id": "bar",
110+
"relates_to_id": "foo",
111+
"relation_type": RelationTypes.THREAD,
112+
},
113+
desc="test_deny_invalid_event",
114+
)
115+
)
116+
channel = self._send_relation(
117+
RelationTypes.THREAD,
118+
EventTypes.Message,
119+
parent_id="foo",
120+
content={"body": "foo", "msgtype": "m.text"},
121+
)
122+
self.assertEquals(200, channel.code, channel.json_body)
123+
124+
def test_deny_invalid_room(self):
125+
"""Test that we deny relations on non-existant events"""
126+
# Create another room and send a message in it.
127+
room2 = self.helper.create_room_as(self.user_id, tok=self.user_token)
128+
res = self.helper.send(room2, body="Hi!", tok=self.user_token)
129+
parent_id = res["event_id"]
130+
131+
# Attempt to send an annotation to that event.
132+
channel = self._send_relation(
133+
RelationTypes.ANNOTATION, "m.reaction", parent_id=parent_id, key="A"
134+
)
135+
self.assertEquals(400, channel.code, channel.json_body)
136+
94137
def test_deny_double_react(self):
95138
"""Test that we deny relations on membership events"""
96139
channel = self._send_relation(RelationTypes.ANNOTATION, "m.reaction", key="a")
@@ -99,6 +142,25 @@ def test_deny_double_react(self):
99142
channel = self._send_relation(RelationTypes.ANNOTATION, "m.reaction", "a")
100143
self.assertEquals(400, channel.code, channel.json_body)
101144

145+
def test_deny_forked_thread(self):
146+
"""It is invalid to start a thread off a thread."""
147+
channel = self._send_relation(
148+
RelationTypes.THREAD,
149+
"m.room.message",
150+
content={"msgtype": "m.text", "body": "foo"},
151+
parent_id=self.parent_id,
152+
)
153+
self.assertEquals(200, channel.code, channel.json_body)
154+
parent_id = channel.json_body["event_id"]
155+
156+
channel = self._send_relation(
157+
RelationTypes.THREAD,
158+
"m.room.message",
159+
content={"msgtype": "m.text", "body": "foo"},
160+
parent_id=parent_id,
161+
)
162+
self.assertEquals(400, channel.code, channel.json_body)
163+
102164
def test_basic_paginate_relations(self):
103165
"""Tests that calling pagination API correctly the latest relations."""
104166
channel = self._send_relation(RelationTypes.ANNOTATION, "m.reaction", "a")

0 commit comments

Comments
 (0)