mirror of
https://mau.dev/maunium/synapse.git
synced 2024-11-10 12:02:43 +01:00
Strictly enforce canonicaljson requirements in a new room version (#7381)
This commit is contained in:
parent
ec0b72bc4e
commit
56b66db78a
7 changed files with 137 additions and 5 deletions
1
changelog.d/7381.bugfix
Normal file
1
changelog.d/7381.bugfix
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Add an experimental room version which strictly adheres to the canonical JSON specification.
|
|
@ -59,7 +59,11 @@ class RoomVersion(object):
|
||||||
|
|
||||||
# bool: before MSC2261/MSC2432, m.room.aliases had special auth rules and redaction rules
|
# bool: before MSC2261/MSC2432, m.room.aliases had special auth rules and redaction rules
|
||||||
special_case_aliases_auth = attr.ib(type=bool)
|
special_case_aliases_auth = attr.ib(type=bool)
|
||||||
|
# Strictly enforce canonicaljson, do not allow:
|
||||||
|
# * Integers outside the range of [-2 ^ 53 + 1, 2 ^ 53 - 1]
|
||||||
|
# * Floats
|
||||||
|
# * NaN, Infinity, -Infinity
|
||||||
|
strict_canonicaljson = attr.ib(type=bool)
|
||||||
# bool: MSC2209: Check 'notifications' key while verifying
|
# bool: MSC2209: Check 'notifications' key while verifying
|
||||||
# m.room.power_levels auth rules.
|
# m.room.power_levels auth rules.
|
||||||
limit_notifications_power_levels = attr.ib(type=bool)
|
limit_notifications_power_levels = attr.ib(type=bool)
|
||||||
|
@ -73,6 +77,7 @@ class RoomVersions(object):
|
||||||
StateResolutionVersions.V1,
|
StateResolutionVersions.V1,
|
||||||
enforce_key_validity=False,
|
enforce_key_validity=False,
|
||||||
special_case_aliases_auth=True,
|
special_case_aliases_auth=True,
|
||||||
|
strict_canonicaljson=False,
|
||||||
limit_notifications_power_levels=False,
|
limit_notifications_power_levels=False,
|
||||||
)
|
)
|
||||||
V2 = RoomVersion(
|
V2 = RoomVersion(
|
||||||
|
@ -82,6 +87,7 @@ class RoomVersions(object):
|
||||||
StateResolutionVersions.V2,
|
StateResolutionVersions.V2,
|
||||||
enforce_key_validity=False,
|
enforce_key_validity=False,
|
||||||
special_case_aliases_auth=True,
|
special_case_aliases_auth=True,
|
||||||
|
strict_canonicaljson=False,
|
||||||
limit_notifications_power_levels=False,
|
limit_notifications_power_levels=False,
|
||||||
)
|
)
|
||||||
V3 = RoomVersion(
|
V3 = RoomVersion(
|
||||||
|
@ -91,6 +97,7 @@ class RoomVersions(object):
|
||||||
StateResolutionVersions.V2,
|
StateResolutionVersions.V2,
|
||||||
enforce_key_validity=False,
|
enforce_key_validity=False,
|
||||||
special_case_aliases_auth=True,
|
special_case_aliases_auth=True,
|
||||||
|
strict_canonicaljson=False,
|
||||||
limit_notifications_power_levels=False,
|
limit_notifications_power_levels=False,
|
||||||
)
|
)
|
||||||
V4 = RoomVersion(
|
V4 = RoomVersion(
|
||||||
|
@ -100,6 +107,7 @@ class RoomVersions(object):
|
||||||
StateResolutionVersions.V2,
|
StateResolutionVersions.V2,
|
||||||
enforce_key_validity=False,
|
enforce_key_validity=False,
|
||||||
special_case_aliases_auth=True,
|
special_case_aliases_auth=True,
|
||||||
|
strict_canonicaljson=False,
|
||||||
limit_notifications_power_levels=False,
|
limit_notifications_power_levels=False,
|
||||||
)
|
)
|
||||||
V5 = RoomVersion(
|
V5 = RoomVersion(
|
||||||
|
@ -109,6 +117,7 @@ class RoomVersions(object):
|
||||||
StateResolutionVersions.V2,
|
StateResolutionVersions.V2,
|
||||||
enforce_key_validity=True,
|
enforce_key_validity=True,
|
||||||
special_case_aliases_auth=True,
|
special_case_aliases_auth=True,
|
||||||
|
strict_canonicaljson=False,
|
||||||
limit_notifications_power_levels=False,
|
limit_notifications_power_levels=False,
|
||||||
)
|
)
|
||||||
MSC2432_DEV = RoomVersion(
|
MSC2432_DEV = RoomVersion(
|
||||||
|
@ -118,6 +127,17 @@ class RoomVersions(object):
|
||||||
StateResolutionVersions.V2,
|
StateResolutionVersions.V2,
|
||||||
enforce_key_validity=True,
|
enforce_key_validity=True,
|
||||||
special_case_aliases_auth=False,
|
special_case_aliases_auth=False,
|
||||||
|
strict_canonicaljson=False,
|
||||||
|
limit_notifications_power_levels=False,
|
||||||
|
)
|
||||||
|
STRICT_CANONICALJSON = RoomVersion(
|
||||||
|
"org.matrix.strict_canonicaljson",
|
||||||
|
RoomDisposition.UNSTABLE,
|
||||||
|
EventFormatVersions.V3,
|
||||||
|
StateResolutionVersions.V2,
|
||||||
|
enforce_key_validity=True,
|
||||||
|
special_case_aliases_auth=True,
|
||||||
|
strict_canonicaljson=True,
|
||||||
limit_notifications_power_levels=False,
|
limit_notifications_power_levels=False,
|
||||||
)
|
)
|
||||||
MSC2209_DEV = RoomVersion(
|
MSC2209_DEV = RoomVersion(
|
||||||
|
@ -127,6 +147,7 @@ class RoomVersions(object):
|
||||||
StateResolutionVersions.V2,
|
StateResolutionVersions.V2,
|
||||||
enforce_key_validity=True,
|
enforce_key_validity=True,
|
||||||
special_case_aliases_auth=True,
|
special_case_aliases_auth=True,
|
||||||
|
strict_canonicaljson=False,
|
||||||
limit_notifications_power_levels=True,
|
limit_notifications_power_levels=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -140,6 +161,7 @@ KNOWN_ROOM_VERSIONS = {
|
||||||
RoomVersions.V4,
|
RoomVersions.V4,
|
||||||
RoomVersions.V5,
|
RoomVersions.V5,
|
||||||
RoomVersions.MSC2432_DEV,
|
RoomVersions.MSC2432_DEV,
|
||||||
|
RoomVersions.STRICT_CANONICALJSON,
|
||||||
RoomVersions.MSC2209_DEV,
|
RoomVersions.MSC2209_DEV,
|
||||||
)
|
)
|
||||||
} # type: Dict[str, RoomVersion]
|
} # type: Dict[str, RoomVersion]
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
import collections
|
import collections
|
||||||
import re
|
import re
|
||||||
from typing import Mapping, Union
|
from typing import Any, Mapping, Union
|
||||||
|
|
||||||
from six import string_types
|
from six import string_types
|
||||||
|
|
||||||
|
@ -23,6 +23,7 @@ from frozendict import frozendict
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
|
||||||
from synapse.api.constants import EventTypes, RelationTypes
|
from synapse.api.constants import EventTypes, RelationTypes
|
||||||
|
from synapse.api.errors import Codes, SynapseError
|
||||||
from synapse.api.room_versions import RoomVersion
|
from synapse.api.room_versions import RoomVersion
|
||||||
from synapse.util.async_helpers import yieldable_gather_results
|
from synapse.util.async_helpers import yieldable_gather_results
|
||||||
|
|
||||||
|
@ -449,3 +450,35 @@ def copy_power_levels_contents(
|
||||||
raise TypeError("Invalid power_levels value for %s: %r" % (k, v))
|
raise TypeError("Invalid power_levels value for %s: %r" % (k, v))
|
||||||
|
|
||||||
return power_levels
|
return power_levels
|
||||||
|
|
||||||
|
|
||||||
|
def validate_canonicaljson(value: Any):
|
||||||
|
"""
|
||||||
|
Ensure that the JSON object is valid according to the rules of canonical JSON.
|
||||||
|
|
||||||
|
See the appendix section 3.1: Canonical JSON.
|
||||||
|
|
||||||
|
This rejects JSON that has:
|
||||||
|
* An integer outside the range of [-2 ^ 53 + 1, 2 ^ 53 - 1]
|
||||||
|
* Floats
|
||||||
|
* NaN, Infinity, -Infinity
|
||||||
|
"""
|
||||||
|
if isinstance(value, int):
|
||||||
|
if value <= -(2 ** 53) or 2 ** 53 <= value:
|
||||||
|
raise SynapseError(400, "JSON integer out of range", Codes.BAD_JSON)
|
||||||
|
|
||||||
|
elif isinstance(value, float):
|
||||||
|
# Note that Infinity, -Infinity, and NaN are also considered floats.
|
||||||
|
raise SynapseError(400, "Bad JSON value: float", Codes.BAD_JSON)
|
||||||
|
|
||||||
|
elif isinstance(value, (dict, frozendict)):
|
||||||
|
for v in value.values():
|
||||||
|
validate_canonicaljson(v)
|
||||||
|
|
||||||
|
elif isinstance(value, (list, tuple)):
|
||||||
|
for i in value:
|
||||||
|
validate_canonicaljson(i)
|
||||||
|
|
||||||
|
elif not isinstance(value, (bool, str)) and value is not None:
|
||||||
|
# Other potential JSON values (bool, None, str) are safe.
|
||||||
|
raise SynapseError(400, "Unknown JSON value", Codes.BAD_JSON)
|
||||||
|
|
|
@ -18,6 +18,7 @@ from six import integer_types, string_types
|
||||||
from synapse.api.constants import MAX_ALIAS_LENGTH, EventTypes, Membership
|
from synapse.api.constants import MAX_ALIAS_LENGTH, EventTypes, Membership
|
||||||
from synapse.api.errors import Codes, SynapseError
|
from synapse.api.errors import Codes, SynapseError
|
||||||
from synapse.api.room_versions import EventFormatVersions
|
from synapse.api.room_versions import EventFormatVersions
|
||||||
|
from synapse.events.utils import validate_canonicaljson
|
||||||
from synapse.types import EventID, RoomID, UserID
|
from synapse.types import EventID, RoomID, UserID
|
||||||
|
|
||||||
|
|
||||||
|
@ -55,6 +56,12 @@ class EventValidator(object):
|
||||||
if not isinstance(getattr(event, s), string_types):
|
if not isinstance(getattr(event, s), string_types):
|
||||||
raise SynapseError(400, "'%s' not a string type" % (s,))
|
raise SynapseError(400, "'%s' not a string type" % (s,))
|
||||||
|
|
||||||
|
# Depending on the room version, ensure the data is spec compliant JSON.
|
||||||
|
if event.room_version.strict_canonicaljson:
|
||||||
|
# Note that only the client controlled portion of the event is
|
||||||
|
# checked, since we trust the portions of the event we created.
|
||||||
|
validate_canonicaljson(event.content)
|
||||||
|
|
||||||
if event.type == EventTypes.Aliases:
|
if event.type == EventTypes.Aliases:
|
||||||
if "aliases" in event.content:
|
if "aliases" in event.content:
|
||||||
for alias in event.content["aliases"]:
|
for alias in event.content["aliases"]:
|
||||||
|
|
|
@ -29,7 +29,7 @@ from synapse.api.room_versions import EventFormatVersions, RoomVersion
|
||||||
from synapse.crypto.event_signing import check_event_content_hash
|
from synapse.crypto.event_signing import check_event_content_hash
|
||||||
from synapse.crypto.keyring import Keyring
|
from synapse.crypto.keyring import Keyring
|
||||||
from synapse.events import EventBase, make_event_from_dict
|
from synapse.events import EventBase, make_event_from_dict
|
||||||
from synapse.events.utils import prune_event
|
from synapse.events.utils import prune_event, validate_canonicaljson
|
||||||
from synapse.http.servlet import assert_params_in_dict
|
from synapse.http.servlet import assert_params_in_dict
|
||||||
from synapse.logging.context import (
|
from synapse.logging.context import (
|
||||||
PreserveLoggingContext,
|
PreserveLoggingContext,
|
||||||
|
@ -302,6 +302,10 @@ def event_from_pdu_json(
|
||||||
elif depth > MAX_DEPTH:
|
elif depth > MAX_DEPTH:
|
||||||
raise SynapseError(400, "Depth too large", Codes.BAD_JSON)
|
raise SynapseError(400, "Depth too large", Codes.BAD_JSON)
|
||||||
|
|
||||||
|
# Validate that the JSON conforms to the specification.
|
||||||
|
if room_version.strict_canonicaljson:
|
||||||
|
validate_canonicaljson(pdu_json)
|
||||||
|
|
||||||
event = make_event_from_dict(pdu_json, room_version)
|
event = make_event_from_dict(pdu_json, room_version)
|
||||||
event.internal_metadata.outlier = outlier
|
event.internal_metadata.outlier = outlier
|
||||||
|
|
||||||
|
|
|
@ -65,5 +65,5 @@ def _handle_frozendict(obj):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# A JSONEncoder which is capable of encoding frozendics without barfing
|
# A JSONEncoder which is capable of encoding frozendicts without barfing
|
||||||
frozendict_json_encoder = json.JSONEncoder(default=_handle_frozendict)
|
frozendict_json_encoder = json.JSONEncoder(default=_handle_frozendict)
|
||||||
|
|
|
@ -13,9 +13,12 @@
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
import logging
|
import logging
|
||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
from synapse.api.constants import EventTypes
|
from synapse.api.constants import EventTypes
|
||||||
from synapse.api.errors import AuthError, Codes
|
from synapse.api.errors import AuthError, Codes, SynapseError
|
||||||
|
from synapse.api.room_versions import RoomVersions
|
||||||
|
from synapse.events import EventBase
|
||||||
from synapse.federation.federation_base import event_from_pdu_json
|
from synapse.federation.federation_base import event_from_pdu_json
|
||||||
from synapse.logging.context import LoggingContext, run_in_background
|
from synapse.logging.context import LoggingContext, run_in_background
|
||||||
from synapse.rest import admin
|
from synapse.rest import admin
|
||||||
|
@ -207,3 +210,65 @@ class FederationTestCase(unittest.HomeserverTestCase):
|
||||||
self.assertEqual(r[(EventTypes.Member, other_user)], join_event.event_id)
|
self.assertEqual(r[(EventTypes.Member, other_user)], join_event.event_id)
|
||||||
|
|
||||||
return join_event
|
return join_event
|
||||||
|
|
||||||
|
|
||||||
|
class EventFromPduTestCase(TestCase):
|
||||||
|
def test_valid_json(self):
|
||||||
|
"""Valid JSON should be turned into an event."""
|
||||||
|
ev = event_from_pdu_json(
|
||||||
|
{
|
||||||
|
"type": EventTypes.Message,
|
||||||
|
"content": {"bool": True, "null": None, "int": 1, "str": "foobar"},
|
||||||
|
"room_id": "!room:test",
|
||||||
|
"sender": "@user:test",
|
||||||
|
"depth": 1,
|
||||||
|
"prev_events": [],
|
||||||
|
"auth_events": [],
|
||||||
|
"origin_server_ts": 1234,
|
||||||
|
},
|
||||||
|
RoomVersions.STRICT_CANONICALJSON,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIsInstance(ev, EventBase)
|
||||||
|
|
||||||
|
def test_invalid_numbers(self):
|
||||||
|
"""Invalid values for an integer should be rejected, all floats should be rejected."""
|
||||||
|
for value in [
|
||||||
|
-(2 ** 53),
|
||||||
|
2 ** 53,
|
||||||
|
1.0,
|
||||||
|
float("inf"),
|
||||||
|
float("-inf"),
|
||||||
|
float("nan"),
|
||||||
|
]:
|
||||||
|
with self.assertRaises(SynapseError):
|
||||||
|
event_from_pdu_json(
|
||||||
|
{
|
||||||
|
"type": EventTypes.Message,
|
||||||
|
"content": {"foo": value},
|
||||||
|
"room_id": "!room:test",
|
||||||
|
"sender": "@user:test",
|
||||||
|
"depth": 1,
|
||||||
|
"prev_events": [],
|
||||||
|
"auth_events": [],
|
||||||
|
"origin_server_ts": 1234,
|
||||||
|
},
|
||||||
|
RoomVersions.STRICT_CANONICALJSON,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_invalid_nested(self):
|
||||||
|
"""List and dictionaries are recursively searched."""
|
||||||
|
with self.assertRaises(SynapseError):
|
||||||
|
event_from_pdu_json(
|
||||||
|
{
|
||||||
|
"type": EventTypes.Message,
|
||||||
|
"content": {"foo": [{"bar": 2 ** 56}]},
|
||||||
|
"room_id": "!room:test",
|
||||||
|
"sender": "@user:test",
|
||||||
|
"depth": 1,
|
||||||
|
"prev_events": [],
|
||||||
|
"auth_events": [],
|
||||||
|
"origin_server_ts": 1234,
|
||||||
|
},
|
||||||
|
RoomVersions.STRICT_CANONICALJSON,
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in a new issue