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

Ensure we use a copy of the event content dict before modifying it in serialize_event #9585

Merged
merged 7 commits into from
Mar 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/9585.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix a longstanding bug that could cause issues when editing a reply to a message.
14 changes: 12 additions & 2 deletions synapse/events/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from synapse.api.errors import Codes, SynapseError
from synapse.api.room_versions import RoomVersion
from synapse.util.async_helpers import yieldable_gather_results
from synapse.util.frozenutils import unfreeze

from . import EventBase

Expand Down Expand Up @@ -400,10 +401,19 @@ async def serialize_event(
# If there is an edit replace the content, preserving existing
# relations.

# Ensure we take copies of the edit content, otherwise we risk modifying
# the original event.
edit_content = edit.content.copy()

# Unfreeze the event content if necessary, so that we may modify it below
edit_content = unfreeze(edit_content)
serialized_event["content"] = edit_content.get("m.new_content", {})

# Check for existing relations
relations = event.content.get("m.relates_to")
serialized_event["content"] = edit.content.get("m.new_content", {})
if relations:
serialized_event["content"]["m.relates_to"] = relations
# Keep the relations, ensuring we use a dict copy of the original
serialized_event["content"]["m.relates_to"] = relations.copy()
else:
serialized_event["content"].pop("m.relates_to", None)

Expand Down
62 changes: 62 additions & 0 deletions tests/rest/client/test_third_party_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,68 @@ async def check(ev: EventBase, state):
ev = channel.json_body
self.assertEqual(ev["content"]["x"], "y")

def test_message_edit(self):
"""Ensure that the module doesn't cause issues with edited messages."""
# first patch the event checker so that it will modify the event
async def check(ev: EventBase, state):
d = ev.get_dict()
d["content"] = {
"msgtype": "m.text",
"body": d["content"]["body"].upper(),
}
return d
Comment on lines +166 to +173
Copy link
Member Author

@anoadragon453 anoadragon453 Mar 17, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit confused as to why we would test that a ThirdPartyRules module can successfully edit an event if we try to prevent modules from modifying events?

# Ensure that the event is frozen, to make sure that the module is not tempted
# to try to modify it. Any attempt to modify it at this point will invalidate
# the hashes and signatures.
event.freeze()

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is this different than the test above that does the same thing? 😕 Maybe I misunderstood what that test was doing...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahaaaaa. It was me who was misunderstanding what the point of freezing the event was.

So we do allow ThirdPartyRules modules to modify event dicts, but it must do so by returning a dict with the new fields, and must not modify the original event object.

So the freeze is just to ensure that last bit - but we still allow modifying the event.

In that case yes, this test looks solid!


current_rules_module().check_event_allowed = check

# Send an event, then edit it.
channel = self.make_request(
"PUT",
"/_matrix/client/r0/rooms/%s/send/modifyme/1" % self.room_id,
{
"msgtype": "m.text",
"body": "Original body",
},
access_token=self.tok,
)
self.assertEqual(channel.result["code"], b"200", channel.result)
orig_event_id = channel.json_body["event_id"]

channel = self.make_request(
"PUT",
"/_matrix/client/r0/rooms/%s/send/m.room.message/2" % self.room_id,
{
"m.new_content": {"msgtype": "m.text", "body": "Edited body"},
"m.relates_to": {
"rel_type": "m.replace",
"event_id": orig_event_id,
},
"msgtype": "m.text",
"body": "Edited body",
},
access_token=self.tok,
)
self.assertEqual(channel.result["code"], b"200", channel.result)
edited_event_id = channel.json_body["event_id"]

# ... and check that they both got modified
channel = self.make_request(
"GET",
"/_matrix/client/r0/rooms/%s/event/%s" % (self.room_id, orig_event_id),
access_token=self.tok,
)
self.assertEqual(channel.result["code"], b"200", channel.result)
ev = channel.json_body
self.assertEqual(ev["content"]["body"], "ORIGINAL BODY")

channel = self.make_request(
"GET",
"/_matrix/client/r0/rooms/%s/event/%s" % (self.room_id, edited_event_id),
access_token=self.tok,
)
self.assertEqual(channel.result["code"], b"200", channel.result)
ev = channel.json_body
self.assertEqual(ev["content"]["body"], "EDITED BODY")

def test_send_event(self):
"""Tests that the module can send an event into a room via the module api"""
content = {
Expand Down
62 changes: 62 additions & 0 deletions tests/rest/client/v2_alpha/test_relations.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ def make_homeserver(self, reactor, clock):
# We need to enable msc1849 support for aggregations
config = self.default_config()
config["experimental_msc1849_support_enabled"] = True

# We enable frozen dicts as relations/edits change event contents, so we
# want to test that we don't modify the events in the caches.
config["use_frozen_dicts"] = True

return self.setup_test_homeserver(config=config)

def prepare(self, reactor, clock, hs):
Expand Down Expand Up @@ -518,6 +523,63 @@ def test_multi_edit(self):
{"event_id": edit_event_id, "sender": self.user_id}, m_replace_dict
)

def test_edit_reply(self):
"""Test that editing a reply works."""

# Create a reply to edit.
channel = self._send_relation(
RelationTypes.REFERENCE,
"m.room.message",
content={"msgtype": "m.text", "body": "A reply!"},
)
self.assertEquals(200, channel.code, channel.json_body)
reply = channel.json_body["event_id"]

new_body = {"msgtype": "m.text", "body": "I've been edited!"}
channel = self._send_relation(
RelationTypes.REPLACE,
"m.room.message",
content={"msgtype": "m.text", "body": "foo", "m.new_content": new_body},
parent_id=reply,
)
self.assertEquals(200, channel.code, channel.json_body)

edit_event_id = channel.json_body["event_id"]

channel = self.make_request(
"GET",
"/rooms/%s/event/%s" % (self.room, reply),
access_token=self.user_token,
)
self.assertEquals(200, channel.code, channel.json_body)

# We expect to see the new body in the dict, as well as the reference
# metadata sill intact.
self.assertDictContainsSubset(new_body, channel.json_body["content"])
self.assertDictContainsSubset(
{
"m.relates_to": {
"event_id": self.parent_id,
"key": None,
"rel_type": "m.reference",
}
},
channel.json_body["content"],
)

# We expect that the edit relation appears in the unsigned relations
# section.
relations_dict = channel.json_body["unsigned"].get("m.relations")
self.assertIn(RelationTypes.REPLACE, relations_dict)

m_replace_dict = relations_dict[RelationTypes.REPLACE]
for key in ["event_id", "sender", "origin_server_ts"]:
self.assertIn(key, m_replace_dict)

self.assert_dict(
{"event_id": edit_event_id, "sender": self.user_id}, m_replace_dict
)

def test_relations_redaction_redacts_edits(self):
"""Test that edits of an event are redacted when the original event
is redacted.
Expand Down
10 changes: 10 additions & 0 deletions tests/unittest.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from twisted.trial import unittest
from twisted.web.resource import Resource

from synapse import events
from synapse.api.constants import EventTypes, Membership
from synapse.config.homeserver import HomeServerConfig
from synapse.config.ratelimiting import FederationRateLimitConfig
Expand Down Expand Up @@ -229,6 +230,11 @@ def setUp(self):
self._hs_args = {"clock": self.clock, "reactor": self.reactor}
self.hs = self.make_homeserver(self.reactor, self.clock)

# Honour the `use_frozen_dicts` config option. We have to do this
# manually because this is taken care of in the app `start` code, which
# we don't run. Plus we want to reset it on tearDown.
events.USE_FROZEN_DICTS = self.hs.config.use_frozen_dicts

if self.hs is None:
raise Exception("No homeserver returned from make_homeserver.")

Expand Down Expand Up @@ -292,6 +298,10 @@ async def get_user_by_req(request, allow_guest=False, rights="access"):
if hasattr(self, "prepare"):
self.prepare(self.reactor, self.clock, self.hs)

def tearDown(self):
# Reset to not use frozen dicts.
events.USE_FROZEN_DICTS = False

def wait_on_thread(self, deferred, timeout=10):
"""
Wait until a Deferred is done, where it's waiting on a real thread.
Expand Down