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

Commit 820337e

Browse files
Require body for read receipts with user-agent exceptions (#11157)
Co-authored-by: reivilibre <olivier@librepush.net>
1 parent 84f235a commit 820337e

3 files changed

Lines changed: 40 additions & 3 deletions

File tree

changelog.d/11157.misc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Only allow old Element/Riot Android clients to send read receipts without a request body. All other clients must include a request body as required by the specification. Contributed by @rogersheu.

synapse/rest/client/receipts.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,21 @@
1313
# limitations under the License.
1414

1515
import logging
16+
import re
1617
from typing import TYPE_CHECKING, Tuple
1718

1819
from synapse.api.constants import ReadReceiptEventFields
1920
from synapse.api.errors import Codes, SynapseError
21+
from synapse.http import get_request_user_agent
2022
from synapse.http.server import HttpServer
2123
from synapse.http.servlet import RestServlet, parse_json_object_from_request
2224
from synapse.http.site import SynapseRequest
2325
from synapse.types import JsonDict
2426

2527
from ._base import client_patterns
2628

29+
pattern = re.compile(r"(?:Element|SchildiChat)/1\.[012]\.")
30+
2731
if TYPE_CHECKING:
2832
from synapse.server import HomeServer
2933

@@ -52,7 +56,13 @@ async def on_POST(
5256
if receipt_type != "m.read":
5357
raise SynapseError(400, "Receipt type must be 'm.read'")
5458

55-
body = parse_json_object_from_request(request, allow_empty_body=True)
59+
# Do not allow older SchildiChat and Element Android clients (prior to Element/1.[012].x) to send an empty body.
60+
user_agent = get_request_user_agent(request)
61+
allow_empty_body = False
62+
if "Android" in user_agent:
63+
if pattern.match(user_agent) or "Riot" in user_agent:
64+
allow_empty_body = True
65+
body = parse_json_object_from_request(request, allow_empty_body)
5666
hidden = body.get(ReadReceiptEventFields.MSC2285_HIDDEN, False)
5767

5868
if not isinstance(hidden, bool):

tests/rest/client/test_sync.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
# limitations under the License.
1515
import json
1616

17+
from parameterized import parameterized
18+
1719
import synapse.rest.admin
1820
from synapse.api.constants import (
1921
EventContentFields,
@@ -417,7 +419,30 @@ def test_hidden_read_receipts(self):
417419
# Test that the first user can't see the other user's hidden read receipt
418420
self.assertEqual(self._get_read_receipt(), None)
419421

420-
def test_read_receipt_with_empty_body(self):
422+
@parameterized.expand(
423+
[
424+
# Old Element version, expected to send an empty body
425+
(
426+
"agent1",
427+
"Element/1.2.2 (Linux; U; Android 9; MatrixAndroidSDK_X 0.0.1)",
428+
200,
429+
),
430+
# Old SchildiChat version, expected to send an empty body
431+
("agent2", "SchildiChat/1.2.1 (Android 10)", 200),
432+
# Expected 400: Denies empty body starting at version 1.3+
433+
("agent3", "Element/1.3.6 (Android 10)", 400),
434+
("agent4", "SchildiChat/1.3.6 (Android 11)", 400),
435+
# Contains "Riot": Receipts with empty bodies expected
436+
("agent5", "Element (Riot.im) (Android 9)", 200),
437+
# Expected 400: Does not contain "Android"
438+
("agent6", "Element/1.2.1", 400),
439+
# Expected 400: Different format, missing "/" after Element; existing build that should allow empty bodies, but minimal ongoing usage
440+
("agent7", "Element dbg/1.1.8-dev (Android)", 400),
441+
]
442+
)
443+
def test_read_receipt_with_empty_body(
444+
self, name, user_agent: str, expected_status_code: int
445+
):
421446
# Send a message as the first user
422447
res = self.helper.send(self.room_id, body="hello", tok=self.tok)
423448

@@ -426,8 +451,9 @@ def test_read_receipt_with_empty_body(self):
426451
"POST",
427452
"/rooms/%s/receipt/m.read/%s" % (self.room_id, res["event_id"]),
428453
access_token=self.tok2,
454+
custom_headers=[("User-Agent", user_agent)],
429455
)
430-
self.assertEqual(channel.code, 200)
456+
self.assertEqual(channel.code, expected_status_code)
431457

432458
def _get_read_receipt(self):
433459
"""Syncs and returns the read receipt."""

0 commit comments

Comments
 (0)