Skip to content

Commit 1d47532

Browse files
TrevisGordananoadragon453
andauthoredApr 18, 2024··
Parse json validation (#16923)
Co-authored-by: Andrew Morgan <[email protected]>
1 parent 09f0957 commit 1d47532

File tree

6 files changed

+220
-47
lines changed

6 files changed

+220
-47
lines changed
 

‎changelog.d/16923.bugfix

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Return `400 M_NOT_JSON` upon receiving invalid JSON in query parameters across various client and admin endpoints, rather than an internal server error.

‎synapse/http/servlet.py

+82
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
import enum
2525
import logging
26+
import urllib.parse as urlparse
2627
from http import HTTPStatus
2728
from typing import (
2829
TYPE_CHECKING,
@@ -450,6 +451,87 @@ def parse_string(
450451
)
451452

452453

454+
def parse_json(
455+
request: Request,
456+
name: str,
457+
default: Optional[dict] = None,
458+
required: bool = False,
459+
encoding: str = "ascii",
460+
) -> Optional[JsonDict]:
461+
"""
462+
Parse a JSON parameter from the request query string.
463+
464+
Args:
465+
request: the twisted HTTP request.
466+
name: the name of the query parameter.
467+
default: value to use if the parameter is absent,
468+
defaults to None.
469+
required: whether to raise a 400 SynapseError if the
470+
parameter is absent, defaults to False.
471+
encoding: The encoding to decode the string content with.
472+
473+
Returns:
474+
A JSON value, or `default` if the named query parameter was not found
475+
and `required` was False.
476+
477+
Raises:
478+
SynapseError if the parameter is absent and required, or if the
479+
parameter is present and not a JSON object.
480+
"""
481+
args: Mapping[bytes, Sequence[bytes]] = request.args # type: ignore
482+
return parse_json_from_args(
483+
args,
484+
name,
485+
default,
486+
required=required,
487+
encoding=encoding,
488+
)
489+
490+
491+
def parse_json_from_args(
492+
args: Mapping[bytes, Sequence[bytes]],
493+
name: str,
494+
default: Optional[dict] = None,
495+
required: bool = False,
496+
encoding: str = "ascii",
497+
) -> Optional[JsonDict]:
498+
"""
499+
Parse a JSON parameter from the request query string.
500+
501+
Args:
502+
args: a mapping of request args as bytes to a list of bytes (e.g. request.args).
503+
name: the name of the query parameter.
504+
default: value to use if the parameter is absent,
505+
defaults to None.
506+
required: whether to raise a 400 SynapseError if the
507+
parameter is absent, defaults to False.
508+
encoding: the encoding to decode the string content with.
509+
510+
A JSON value, or `default` if the named query parameter was not found
511+
and `required` was False.
512+
513+
Raises:
514+
SynapseError if the parameter is absent and required, or if the
515+
parameter is present and not a JSON object.
516+
"""
517+
name_bytes = name.encode("ascii")
518+
519+
if name_bytes not in args:
520+
if not required:
521+
return default
522+
523+
message = f"Missing required integer query parameter {name}"
524+
raise SynapseError(HTTPStatus.BAD_REQUEST, message, errcode=Codes.MISSING_PARAM)
525+
526+
json_str = parse_string_from_args(args, name, required=True, encoding=encoding)
527+
528+
try:
529+
return json_decoder.decode(urlparse.unquote(json_str))
530+
except Exception:
531+
message = f"Query parameter {name} must be a valid JSON object"
532+
raise SynapseError(HTTPStatus.BAD_REQUEST, message, errcode=Codes.NOT_JSON)
533+
534+
453535
EnumT = TypeVar("EnumT", bound=enum.Enum)
454536

455537

‎synapse/rest/admin/rooms.py

+12-24
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
import logging
2222
from http import HTTPStatus
2323
from typing import TYPE_CHECKING, List, Optional, Tuple, cast
24-
from urllib import parse as urlparse
2524

2625
import attr
2726

@@ -38,6 +37,7 @@
3837
assert_params_in_dict,
3938
parse_enum,
4039
parse_integer,
40+
parse_json,
4141
parse_json_object_from_request,
4242
parse_string,
4343
)
@@ -51,7 +51,6 @@
5151
from synapse.streams.config import PaginationConfig
5252
from synapse.types import JsonDict, RoomID, ScheduledTask, UserID, create_requester
5353
from synapse.types.state import StateFilter
54-
from synapse.util import json_decoder
5554

5655
if TYPE_CHECKING:
5756
from synapse.api.auth import Auth
@@ -776,14 +775,8 @@ async def on_GET(
776775
limit = parse_integer(request, "limit", default=10)
777776

778777
# picking the API shape for symmetry with /messages
779-
filter_str = parse_string(request, "filter", encoding="utf-8")
780-
if filter_str:
781-
filter_json = urlparse.unquote(filter_str)
782-
event_filter: Optional[Filter] = Filter(
783-
self._hs, json_decoder.decode(filter_json)
784-
)
785-
else:
786-
event_filter = None
778+
filter_json = parse_json(request, "filter", encoding="utf-8")
779+
event_filter = Filter(self._hs, filter_json) if filter_json else None
787780

788781
event_context = await self.room_context_handler.get_event_context(
789782
requester,
@@ -914,21 +907,16 @@ async def on_GET(
914907
)
915908
# Twisted will have processed the args by now.
916909
assert request.args is not None
910+
911+
filter_json = parse_json(request, "filter", encoding="utf-8")
912+
event_filter = Filter(self._hs, filter_json) if filter_json else None
913+
917914
as_client_event = b"raw" not in request.args
918-
filter_str = parse_string(request, "filter", encoding="utf-8")
919-
if filter_str:
920-
filter_json = urlparse.unquote(filter_str)
921-
event_filter: Optional[Filter] = Filter(
922-
self._hs, json_decoder.decode(filter_json)
923-
)
924-
if (
925-
event_filter
926-
and event_filter.filter_json.get("event_format", "client")
927-
== "federation"
928-
):
929-
as_client_event = False
930-
else:
931-
event_filter = None
915+
if (
916+
event_filter
917+
and event_filter.filter_json.get("event_format", "client") == "federation"
918+
):
919+
as_client_event = False
932920

933921
msgs = await self._pagination_handler.get_messages(
934922
room_id=room_id,

‎synapse/rest/client/room.py

+12-23
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
parse_boolean,
5353
parse_enum,
5454
parse_integer,
55+
parse_json,
5556
parse_json_object_from_request,
5657
parse_string,
5758
parse_strings_from_args,
@@ -65,7 +66,6 @@
6566
from synapse.streams.config import PaginationConfig
6667
from synapse.types import JsonDict, Requester, StreamToken, ThirdPartyInstanceID, UserID
6768
from synapse.types.state import StateFilter
68-
from synapse.util import json_decoder
6969
from synapse.util.cancellation import cancellable
7070
from synapse.util.stringutils import parse_and_validate_server_name, random_string
7171

@@ -703,21 +703,16 @@ async def on_GET(
703703
)
704704
# Twisted will have processed the args by now.
705705
assert request.args is not None
706+
707+
filter_json = parse_json(request, "filter", encoding="utf-8")
708+
event_filter = Filter(self._hs, filter_json) if filter_json else None
709+
706710
as_client_event = b"raw" not in request.args
707-
filter_str = parse_string(request, "filter", encoding="utf-8")
708-
if filter_str:
709-
filter_json = urlparse.unquote(filter_str)
710-
event_filter: Optional[Filter] = Filter(
711-
self._hs, json_decoder.decode(filter_json)
712-
)
713-
if (
714-
event_filter
715-
and event_filter.filter_json.get("event_format", "client")
716-
== "federation"
717-
):
718-
as_client_event = False
719-
else:
720-
event_filter = None
711+
if (
712+
event_filter
713+
and event_filter.filter_json.get("event_format", "client") == "federation"
714+
):
715+
as_client_event = False
721716

722717
msgs = await self.pagination_handler.get_messages(
723718
room_id=room_id,
@@ -898,14 +893,8 @@ async def on_GET(
898893
limit = parse_integer(request, "limit", default=10)
899894

900895
# picking the API shape for symmetry with /messages
901-
filter_str = parse_string(request, "filter", encoding="utf-8")
902-
if filter_str:
903-
filter_json = urlparse.unquote(filter_str)
904-
event_filter: Optional[Filter] = Filter(
905-
self._hs, json_decoder.decode(filter_json)
906-
)
907-
else:
908-
event_filter = None
896+
filter_json = parse_json(request, "filter", encoding="utf-8")
897+
event_filter = Filter(self._hs, filter_json) if filter_json else None
909898

910899
event_context = await self.room_context_handler.get_event_context(
911900
requester, room_id, event_id, limit, event_filter

‎tests/rest/admin/test_room.py

+61
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import json
2222
import time
2323
import urllib.parse
24+
from http import HTTPStatus
2425
from typing import List, Optional
2526
from unittest.mock import AsyncMock, Mock
2627

@@ -2190,6 +2191,33 @@ def test_room_messages_purge(self) -> None:
21902191
chunk = channel.json_body["chunk"]
21912192
self.assertEqual(len(chunk), 0, [event["content"] for event in chunk])
21922193

2194+
def test_room_message_filter_query_validation(self) -> None:
2195+
# Test json validation in (filter) query parameter.
2196+
# Does not test the validity of the filter, only the json validation.
2197+
2198+
# Check Get with valid json filter parameter, expect 200.
2199+
valid_filter_str = '{"types": ["m.room.message"]}'
2200+
channel = self.make_request(
2201+
"GET",
2202+
f"/_synapse/admin/v1/rooms/{self.room_id}/messages?dir=b&filter={valid_filter_str}",
2203+
access_token=self.admin_user_tok,
2204+
)
2205+
2206+
self.assertEqual(channel.code, HTTPStatus.OK, channel.json_body)
2207+
2208+
# Check Get with invalid json filter parameter, expect 400 NOT_JSON.
2209+
invalid_filter_str = "}}}{}"
2210+
channel = self.make_request(
2211+
"GET",
2212+
f"/_synapse/admin/v1/rooms/{self.room_id}/messages?dir=b&filter={invalid_filter_str}",
2213+
access_token=self.admin_user_tok,
2214+
)
2215+
2216+
self.assertEqual(channel.code, HTTPStatus.BAD_REQUEST, channel.json_body)
2217+
self.assertEqual(
2218+
channel.json_body["errcode"], Codes.NOT_JSON, channel.json_body
2219+
)
2220+
21932221

21942222
class JoinAliasRoomTestCase(unittest.HomeserverTestCase):
21952223
servlets = [
@@ -2522,6 +2550,39 @@ def test_context_as_admin(self) -> None:
25222550
else:
25232551
self.fail("Event %s from events_after not found" % j)
25242552

2553+
def test_room_event_context_filter_query_validation(self) -> None:
2554+
# Test json validation in (filter) query parameter.
2555+
# Does not test the validity of the filter, only the json validation.
2556+
2557+
# Create a user with room and event_id.
2558+
user_id = self.register_user("test", "test")
2559+
user_tok = self.login("test", "test")
2560+
room_id = self.helper.create_room_as(user_id, tok=user_tok)
2561+
event_id = self.helper.send(room_id, "message 1", tok=user_tok)["event_id"]
2562+
2563+
# Check Get with valid json filter parameter, expect 200.
2564+
valid_filter_str = '{"types": ["m.room.message"]}'
2565+
channel = self.make_request(
2566+
"GET",
2567+
f"/_synapse/admin/v1/rooms/{room_id}/context/{event_id}?filter={valid_filter_str}",
2568+
access_token=self.admin_user_tok,
2569+
)
2570+
2571+
self.assertEqual(channel.code, HTTPStatus.OK, channel.json_body)
2572+
2573+
# Check Get with invalid json filter parameter, expect 400 NOT_JSON.
2574+
invalid_filter_str = "}}}{}"
2575+
channel = self.make_request(
2576+
"GET",
2577+
f"/_synapse/admin/v1/rooms/{room_id}/context/{event_id}?filter={invalid_filter_str}",
2578+
access_token=self.admin_user_tok,
2579+
)
2580+
2581+
self.assertEqual(channel.code, HTTPStatus.BAD_REQUEST, channel.json_body)
2582+
self.assertEqual(
2583+
channel.json_body["errcode"], Codes.NOT_JSON, channel.json_body
2584+
)
2585+
25252586

25262587
class MakeRoomAdminTestCase(unittest.HomeserverTestCase):
25272588
servlets = [

‎tests/rest/client/test_rooms.py

+52
Original file line numberDiff line numberDiff line change
@@ -2175,6 +2175,31 @@ def test_room_messages_purge(self) -> None:
21752175
chunk = channel.json_body["chunk"]
21762176
self.assertEqual(len(chunk), 0, [event["content"] for event in chunk])
21772177

2178+
def test_room_message_filter_query_validation(self) -> None:
2179+
# Test json validation in (filter) query parameter.
2180+
# Does not test the validity of the filter, only the json validation.
2181+
2182+
# Check Get with valid json filter parameter, expect 200.
2183+
valid_filter_str = '{"types": ["m.room.message"]}'
2184+
channel = self.make_request(
2185+
"GET",
2186+
f"/rooms/{self.room_id}/messages?access_token=x&dir=b&filter={valid_filter_str}",
2187+
)
2188+
2189+
self.assertEqual(channel.code, HTTPStatus.OK, channel.json_body)
2190+
2191+
# Check Get with invalid json filter parameter, expect 400 NOT_JSON.
2192+
invalid_filter_str = "}}}{}"
2193+
channel = self.make_request(
2194+
"GET",
2195+
f"/rooms/{self.room_id}/messages?access_token=x&dir=b&filter={invalid_filter_str}",
2196+
)
2197+
2198+
self.assertEqual(channel.code, HTTPStatus.BAD_REQUEST, channel.json_body)
2199+
self.assertEqual(
2200+
channel.json_body["errcode"], Codes.NOT_JSON, channel.json_body
2201+
)
2202+
21782203

21792204
class RoomMessageFilterTestCase(RoomBase):
21802205
"""Tests /rooms/$room_id/messages REST events."""
@@ -3213,6 +3238,33 @@ def test_erased_sender(self) -> None:
32133238
self.assertDictEqual(events_after[0].get("content"), {}, events_after[0])
32143239
self.assertEqual(events_after[1].get("content"), {}, events_after[1])
32153240

3241+
def test_room_event_context_filter_query_validation(self) -> None:
3242+
# Test json validation in (filter) query parameter.
3243+
# Does not test the validity of the filter, only the json validation.
3244+
event_id = self.helper.send(self.room_id, "message 7", tok=self.tok)["event_id"]
3245+
3246+
# Check Get with valid json filter parameter, expect 200.
3247+
valid_filter_str = '{"types": ["m.room.message"]}'
3248+
channel = self.make_request(
3249+
"GET",
3250+
f"/rooms/{self.room_id}/context/{event_id}?filter={valid_filter_str}",
3251+
access_token=self.tok,
3252+
)
3253+
self.assertEqual(channel.code, HTTPStatus.OK, channel.json_body)
3254+
3255+
# Check Get with invalid json filter parameter, expect 400 NOT_JSON.
3256+
invalid_filter_str = "}}}{}"
3257+
channel = self.make_request(
3258+
"GET",
3259+
f"/rooms/{self.room_id}/context/{event_id}?filter={invalid_filter_str}",
3260+
access_token=self.tok,
3261+
)
3262+
3263+
self.assertEqual(channel.code, HTTPStatus.BAD_REQUEST, channel.json_body)
3264+
self.assertEqual(
3265+
channel.json_body["errcode"], Codes.NOT_JSON, channel.json_body
3266+
)
3267+
32163268

32173269
class RoomAliasListTestCase(unittest.HomeserverTestCase):
32183270
servlets = [

0 commit comments

Comments
 (0)
Please sign in to comment.