Strictly enforce canonicaljson requirements in a new room version (#7381)

This commit is contained in:
Patrick Cloke 2020-05-14 13:24:01 -04:00 committed by GitHub
parent ec0b72bc4e
commit 56b66db78a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 137 additions and 5 deletions

1
changelog.d/7381.bugfix Normal file
View File

@ -0,0 +1 @@
Add an experimental room version which strictly adheres to the canonical JSON specification.

View File

@ -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]

View File

@ -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)

View File

@ -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"]:

View File

@ -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

View File

@ -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)

View File

@ -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,
)