mirror of
https://github.com/element-hq/synapse.git
synced 2026-05-24 15:15:22 +00:00
Fix validation of frozen message event with mentions. (#19634)
Fixes: #19689 # What This PR fixes a bug I found when I run synapse (from dockerhub) and register a `check_event_allowed` callback and my client makes use of the mentions field in messages (`cinny:latest`). The bug doesn't appear when the `check_event_allowed` callback is not loaded. After some digging I noticed that the current validation of the mentions doesn't work when an event has been frozen with `event.freeze()`. For the messages this seems to happen when a the `check_event_allowed` is registered (but not otherwise), see [where the event is frozen for check_event_allowed callback](https://github.com/element-hq/synapse/blob/b0fc0b7a612a42e6f15b87dee2a1db4c383645fb/synapse/module_api/callbacks/third_party_event_rules_callbacks.py#L289) and [where the validation function is called](https://github.com/element-hq/synapse/blob/b0fc0b7a612a42e6f15b87dee2a1db4c383645fb/synapse/handlers/message.py#L1404). To have a minimal reproduction example, the following scripts fails on `develop` but succeeds in this branch: ``` python from synapse.api.room_versions import RoomVersions from synapse.events import EventBase, make_event_from_dict from synapse.events.validator import EventValidator from tests.utils import default_config def make_message_event(content: dict) -> EventBase: return make_event_from_dict( { "room_id": "!room:test", "type": "m.room.message", "sender": "@alice:test", "content": content, "auth_events": [], "prev_events": [], "hashes": {"sha256": "aGVsbG8="}, "signatures": {}, "depth": 1, "origin_server_ts": 1000, }, room_version=RoomVersions.V9, ) event = make_message_event( { "msgtype": "m.text", "body": "@moderator:example.com hello", "m.mentions": {"user_ids": ["@moderator:jailbreak-challenge.aqtiveguard.com"]}, } ) EventValidator().validate_new(event, default_config) # Ok event.freeze() EventValidator().validate_new(event, default_config) # throws # pydantic_core._pydantic_core.ValidationError: 1 validation error for Mentions # Input should be a valid dictionary or instance of Mentions [type=model_type, input_value=immutabledict({'user_ids'...nge.aqtiveguard.com',)}), input_type=immutabledict] # For further information visit https://errors.pydantic.dev/2.12/v/model_type ``` # How I made the validation logic also validate the transformation performed by the freezing process, namely: - `immutabledict` validates as `dict`. (was already implemented for POWER_LEVELS) - `tuple` validates as array (added this to the validator in this PR). --------- Co-authored-by: Eric Eastwood <madlittlemods@gmail.com> Co-authored-by: Olivier 'reivilibre <oliverw@matrix.org>
This commit is contained in:
@@ -0,0 +1 @@
|
||||
Fix bug where Synapse would return 400 (`M_BAD_JSON`) when sending a message with `mentions` field and Synapse module `check_event_allowed` callback registered (frozen event). Contributed by @gaetan-sbt.
|
||||
+52
-29
@@ -22,7 +22,6 @@ import collections.abc
|
||||
from typing import cast
|
||||
|
||||
import jsonschema
|
||||
from pydantic import Field, StrictBool, StrictStr
|
||||
|
||||
from synapse.api.constants import (
|
||||
MAX_ALIAS_LENGTH,
|
||||
@@ -40,10 +39,8 @@ from synapse.events.utils import (
|
||||
CANONICALJSON_MIN_INT,
|
||||
validate_canonicaljson,
|
||||
)
|
||||
from synapse.http.servlet import validate_json_object
|
||||
from synapse.storage.controllers.state import server_acl_evaluator_from_event
|
||||
from synapse.types import EventID, JsonDict, JsonMapping, RoomID, StrCollection, UserID
|
||||
from synapse.types.rest import RequestBodyModel
|
||||
|
||||
|
||||
class EventValidator:
|
||||
@@ -116,29 +113,18 @@ class EventValidator:
|
||||
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
|
||||
# to see any arrays.
|
||||
message = (
|
||||
'"' + cast(str, e.path[-1]) + '": ' + e.message # noqa: B306
|
||||
)
|
||||
# jsonschema.ValidationError.message is a valid attribute
|
||||
else:
|
||||
# example: '0' is not of type 'integer'
|
||||
message = e.message # noqa: B306
|
||||
# jsonschema.ValidationError.message is a valid attribute
|
||||
|
||||
raise SynapseError(
|
||||
code=400,
|
||||
msg=message,
|
||||
errcode=Codes.BAD_JSON,
|
||||
)
|
||||
raise _validation_error_to_api_error(e)
|
||||
|
||||
# If the event contains a mentions key, validate it.
|
||||
if EventContentFields.MENTIONS in event.content:
|
||||
validate_json_object(event.content[EventContentFields.MENTIONS], Mentions)
|
||||
try:
|
||||
jsonschema.validate(
|
||||
instance=event.content[EventContentFields.MENTIONS],
|
||||
schema=MENTIONS_SCHEMA,
|
||||
cls=MENTIONS_VALIDATOR,
|
||||
)
|
||||
except jsonschema.ValidationError as e:
|
||||
raise _validation_error_to_api_error(e)
|
||||
|
||||
def _validate_retention(self, event: EventBase) -> None:
|
||||
"""Checks that an event that defines the retention policy for a room respects the
|
||||
@@ -284,10 +270,16 @@ POWER_LEVELS_SCHEMA = {
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class Mentions(RequestBodyModel):
|
||||
user_ids: list[StrictStr] = Field(default_factory=list)
|
||||
room: StrictBool = False
|
||||
MENTIONS_SCHEMA = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"user_ids": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"room": {"type": "boolean"},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# This could return something newer than Draft 7, but that's the current "latest"
|
||||
@@ -295,14 +287,45 @@ class Mentions(RequestBodyModel):
|
||||
def _create_validator(schema: JsonDict) -> type[jsonschema.Draft7Validator]:
|
||||
validator = jsonschema.validators.validator_for(schema)
|
||||
|
||||
# by default jsonschema does not consider a immutabledict to be an object so
|
||||
# we need to use a custom type checker
|
||||
# by default jsonschema does not consider a immutabledict to be an object, or
|
||||
# a tuple to be an array (frozenutils freezes lists to tuples), so we need a
|
||||
# custom type checker for both.
|
||||
# https://python-jsonschema.readthedocs.io/en/stable/validate/?highlight=object#validating-with-additional-types
|
||||
type_checker = validator.TYPE_CHECKER.redefine(
|
||||
"object", lambda checker, thing: isinstance(thing, collections.abc.Mapping)
|
||||
).redefine(
|
||||
"array",
|
||||
lambda checker, thing: isinstance(thing, collections.abc.Sequence),
|
||||
)
|
||||
|
||||
return jsonschema.validators.extend(validator, type_checker=type_checker)
|
||||
|
||||
|
||||
def _validation_error_to_api_error(err: jsonschema.ValidationError) -> SynapseError:
|
||||
"""
|
||||
Converts a JSONSchema `ValidationError` to a `SynapseError` that can be thrown
|
||||
to give a Matrix API-compatible 400 Bad Request response with `M_BAD_JSON` code
|
||||
and a descriptive error message.
|
||||
"""
|
||||
if err.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
|
||||
# to see any arrays.
|
||||
message = '"' + cast(str, err.path[-1]) + '": ' + err.message
|
||||
# jsonschema.ValidationError.message is a valid attribute
|
||||
else:
|
||||
# example: '0' is not of type 'integer'
|
||||
message = err.message
|
||||
# jsonschema.ValidationError.message is a valid attribute
|
||||
|
||||
return SynapseError(
|
||||
code=400,
|
||||
msg=message,
|
||||
errcode=Codes.BAD_JSON,
|
||||
)
|
||||
|
||||
|
||||
POWER_LEVELS_VALIDATOR = _create_validator(POWER_LEVELS_SCHEMA)
|
||||
|
||||
MENTIONS_VALIDATOR = _create_validator(MENTIONS_SCHEMA)
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
#
|
||||
# This file is licensed under the Affero General Public License (AGPL) version 3.
|
||||
#
|
||||
# Copyright (C) 2026 Element Creations Ltd
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# See the GNU Affero General Public License for more details:
|
||||
# <https://www.gnu.org/licenses/agpl-3.0.html>.
|
||||
#
|
||||
from synapse.api.room_versions import RoomVersions
|
||||
from synapse.events import make_event_from_dict
|
||||
from synapse.events.validator import EventValidator
|
||||
|
||||
from tests.unittest import HomeserverTestCase
|
||||
|
||||
|
||||
class EventValidatorTestCase(HomeserverTestCase):
|
||||
def test_validate_new_with_mentions_succeeds_even_when_frozen(self) -> None:
|
||||
"""
|
||||
Test that `EventValidator.validate_new` accepts an event with valid `m.mentions`
|
||||
content even when the event is frozen.
|
||||
"""
|
||||
event = make_event_from_dict(
|
||||
{
|
||||
"room_id": "!room:test",
|
||||
"type": "m.room.message",
|
||||
"sender": "@alice:example.com",
|
||||
"content": {
|
||||
"msgtype": "m.text",
|
||||
"body": "@alice:example.com hello",
|
||||
"m.mentions": {"user_ids": ["@alice:example.com"]},
|
||||
},
|
||||
"auth_events": [],
|
||||
"prev_events": [],
|
||||
"hashes": {"sha256": "aGVsbG8="},
|
||||
"signatures": {},
|
||||
"depth": 1,
|
||||
"origin_server_ts": 1000,
|
||||
},
|
||||
room_version=RoomVersions.V9,
|
||||
)
|
||||
# Sanity check that the event is valid before freezing
|
||||
EventValidator().validate_new(event, self.hs.config)
|
||||
event.freeze()
|
||||
# Event should still be valid after freezing
|
||||
EventValidator().validate_new(event, self.hs.config)
|
||||
Reference in New Issue
Block a user