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

Reject mentions on the C-S API which are invalid. #15311

Merged
merged 3 commits into from
Mar 24, 2023
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/15311.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Reject events with an invalid "mentions" property pert [MSC3952](https://github.com/matrix-org/matrix-spec-proposals/pull/3952).
42 changes: 32 additions & 10 deletions synapse/events/validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,17 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import collections.abc
from typing import Iterable, Type, Union, cast
from typing import Iterable, List, Type, Union, cast

import jsonschema
from pydantic import Field, StrictBool, StrictStr

from synapse.api.constants import MAX_ALIAS_LENGTH, EventTypes, Membership
from synapse.api.constants import (
MAX_ALIAS_LENGTH,
EventContentFields,
EventTypes,
Membership,
)
from synapse.api.errors import Codes, SynapseError
from synapse.api.room_versions import EventFormatVersions
from synapse.config.homeserver import HomeServerConfig
Expand All @@ -28,6 +34,8 @@
validate_canonicaljson,
)
from synapse.federation.federation_server import server_matches_acl_event
from synapse.http.servlet import validate_json_object
from synapse.rest.models import RequestBodyModel
from synapse.types import EventID, JsonDict, RoomID, UserID


Expand Down Expand Up @@ -88,27 +96,27 @@ def validate_new(self, event: EventBase, config: HomeServerConfig) -> None:
Codes.INVALID_PARAM,
)

if event.type == EventTypes.Retention:
elif event.type == EventTypes.Retention:
self._validate_retention(event)

if event.type == EventTypes.ServerACL:
elif event.type == EventTypes.ServerACL:
if not server_matches_acl_event(config.server.server_name, event):
raise SynapseError(
400, "Can't create an ACL event that denies the local server"
)

if event.type == EventTypes.PowerLevels:
elif event.type == EventTypes.PowerLevels:
try:
jsonschema.validate(
instance=event.content,
schema=POWER_LEVELS_SCHEMA,
cls=plValidator,
cls=POWER_LEVELS_VALIDATOR,
)
except jsonschema.ValidationError as e:
if e.path:
# example: "users_default": '0' is not of type 'integer'
# cast safety: path entries can be integers, if we fail to validate
# items in an array. However the POWER_LEVELS_SCHEMA doesn't expect
# items in an array. However, the POWER_LEVELS_SCHEMA doesn't expect
# to see any arrays.
message = (
'"' + cast(str, e.path[-1]) + '": ' + e.message # noqa: B306
Expand All @@ -125,6 +133,15 @@ def validate_new(self, event: EventBase, config: HomeServerConfig) -> None:
errcode=Codes.BAD_JSON,
)

# If the event contains a mentions key, validate it.
if (
EventContentFields.MSC3952_MENTIONS in event.content
and config.experimental.msc3952_intentional_mentions
):
validate_json_object(
event.content[EventContentFields.MSC3952_MENTIONS], Mentions
)

def _validate_retention(self, event: EventBase) -> None:
"""Checks that an event that defines the retention policy for a room respects the
format enforced by the spec.
Expand Down Expand Up @@ -253,10 +270,15 @@ def _ensure_state_event(self, event: Union[EventBase, EventBuilder]) -> None:
}


class Mentions(RequestBodyModel):
user_ids: List[StrictStr] = Field(default_factory=list)
room: StrictBool = False


# This could return something newer than Draft 7, but that's the current "latest"
# validator.
def _create_power_level_validator() -> Type[jsonschema.Draft7Validator]:
validator = jsonschema.validators.validator_for(POWER_LEVELS_SCHEMA)
def _create_validator(schema: JsonDict) -> Type[jsonschema.Draft7Validator]:
validator = jsonschema.validators.validator_for(schema)

# by default jsonschema does not consider a frozendict to be an object so
# we need to use a custom type checker
Expand All @@ -268,4 +290,4 @@ def _create_power_level_validator() -> Type[jsonschema.Draft7Validator]:
return jsonschema.validators.extend(validator, type_checker=type_checker)


plValidator = _create_power_level_validator()
POWER_LEVELS_VALIDATOR = _create_validator(POWER_LEVELS_SCHEMA)
22 changes: 16 additions & 6 deletions synapse/http/servlet.py
Original file line number Diff line number Diff line change
Expand Up @@ -778,17 +778,13 @@ def parse_json_object_from_request(
Model = TypeVar("Model", bound=BaseModel)


def parse_and_validate_json_object_from_request(
request: Request, model_type: Type[Model]
) -> Model:
"""Parse a JSON object from the body of a twisted HTTP request, then deserialise and
validate using the given pydantic model.
def validate_json_object(content: JsonDict, model_type: Type[Model]) -> Model:
"""Validate a deserialized JSON object using the given pydantic model.

Raises:
SynapseError if the request body couldn't be decoded as JSON or
if it wasn't a JSON object.
"""
content = parse_json_object_from_request(request, allow_empty_body=False)
try:
instance = model_type.parse_obj(content)
except ValidationError as e:
Expand All @@ -811,6 +807,20 @@ def parse_and_validate_json_object_from_request(
return instance


def parse_and_validate_json_object_from_request(
request: Request, model_type: Type[Model]
) -> Model:
"""Parse a JSON object from the body of a twisted HTTP request, then deserialise and
validate using the given pydantic model.

Raises:
SynapseError if the request body couldn't be decoded as JSON or
if it wasn't a JSON object.
"""
content = parse_json_object_from_request(request, allow_empty_body=False)
return validate_json_object(content, model_type)


def assert_params_in_dict(body: JsonDict, required: Iterable[str]) -> None:
absent = []
for k in required:
Expand Down
94 changes: 56 additions & 38 deletions tests/push/test_bulk_push_rule_evaluator.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,22 +243,28 @@ def test_user_mentions(self) -> None:
)

# Non-dict mentions should be ignored.
mentions: Any
for mentions in (None, True, False, 1, "foo", []):
self.assertFalse(
self._create_and_process(
bulk_evaluator, {EventContentFields.MSC3952_MENTIONS: mentions}
#
# Avoid C-S validation as these aren't expected.
with patch(
"synapse.events.validator.EventValidator.validate_new",
new=lambda s, event, config: True,
):
mentions: Any
for mentions in (None, True, False, 1, "foo", []):
self.assertFalse(
self._create_and_process(
bulk_evaluator, {EventContentFields.MSC3952_MENTIONS: mentions}
)
)
)

# A non-list should be ignored.
for mentions in (None, True, False, 1, "foo", {}):
self.assertFalse(
self._create_and_process(
bulk_evaluator,
{EventContentFields.MSC3952_MENTIONS: {"user_ids": mentions}},
# A non-list should be ignored.
for mentions in (None, True, False, 1, "foo", {}):
self.assertFalse(
self._create_and_process(
bulk_evaluator,
{EventContentFields.MSC3952_MENTIONS: {"user_ids": mentions}},
)
)
)

# The Matrix ID appearing anywhere in the list should notify.
self.assertTrue(
Expand Down Expand Up @@ -291,26 +297,32 @@ def test_user_mentions(self) -> None:
)

# Invalid entries in the list are ignored.
self.assertFalse(
self._create_and_process(
bulk_evaluator,
{
EventContentFields.MSC3952_MENTIONS: {
"user_ids": [None, True, False, {}, []]
}
},
#
# Avoid C-S validation as these aren't expected.
with patch(
"synapse.events.validator.EventValidator.validate_new",
new=lambda s, event, config: True,
):
self.assertFalse(
self._create_and_process(
bulk_evaluator,
{
EventContentFields.MSC3952_MENTIONS: {
"user_ids": [None, True, False, {}, []]
}
},
)
)
)
self.assertTrue(
self._create_and_process(
bulk_evaluator,
{
EventContentFields.MSC3952_MENTIONS: {
"user_ids": [None, True, False, {}, [], self.alice]
}
},
self.assertTrue(
self._create_and_process(
bulk_evaluator,
{
EventContentFields.MSC3952_MENTIONS: {
"user_ids": [None, True, False, {}, [], self.alice]
}
},
)
)
)

# The legacy push rule should not mention if the mentions field exists.
self.assertFalse(
Expand Down Expand Up @@ -351,14 +363,20 @@ def test_room_mentions(self) -> None:
)

# Invalid data should not notify.
mentions: Any
for mentions in (None, False, 1, "foo", [], {}):
self.assertFalse(
self._create_and_process(
bulk_evaluator,
{EventContentFields.MSC3952_MENTIONS: {"room": mentions}},
#
# Avoid C-S validation as these aren't expected.
with patch(
"synapse.events.validator.EventValidator.validate_new",
new=lambda s, event, config: True,
):
mentions: Any
for mentions in (None, False, 1, "foo", [], {}):
self.assertFalse(
self._create_and_process(
bulk_evaluator,
{EventContentFields.MSC3952_MENTIONS: {"room": mentions}},
)
)
)

# The legacy push rule should not mention if the mentions field exists.
self.assertFalse(
Expand Down