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

Validate new m.room.power_levels events #10232

Merged
merged 20 commits into from
Aug 26, 2021
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/10232.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Validate new `m.room.power_levels` events. Contributed by @aaronraimist.
5 changes: 4 additions & 1 deletion synapse/events/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@
# the literal fields "foo\" and "bar" but will instead be treated as "foo\\.bar"
SPLIT_FIELD_REGEX = re.compile(r"(?<!\\)\.")

CANONICALJSON_MAX_INT = (2 ** 53) - 1
CANONICALJSON_MIN_INT = -CANONICALJSON_MAX_INT


def prune_event(event: EventBase) -> EventBase:
"""Returns a pruned version of the given event, which removes all keys we
Expand Down Expand Up @@ -505,7 +508,7 @@ def validate_canonicaljson(value: Any):
* NaN, Infinity, -Infinity
"""
if isinstance(value, int):
if value <= -(2 ** 53) or 2 ** 53 <= value:
if value < CANONICALJSON_MIN_INT or CANONICALJSON_MAX_INT < value:
raise SynapseError(400, "JSON integer out of range", Codes.BAD_JSON)

elif isinstance(value, float):
Expand Down
77 changes: 75 additions & 2 deletions synapse/events/validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,22 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import collections.abc
from typing import Union

import jsonschema

from synapse.api.constants import MAX_ALIAS_LENGTH, EventTypes, Membership
from synapse.api.errors import Codes, SynapseError
from synapse.api.room_versions import EventFormatVersions
from synapse.config.homeserver import HomeServerConfig
from synapse.events import EventBase
from synapse.events.builder import EventBuilder
from synapse.events.utils import validate_canonicaljson
from synapse.events.utils import (
CANONICALJSON_MAX_INT,
CANONICALJSON_MIN_INT,
validate_canonicaljson,
)
from synapse.federation.federation_server import server_matches_acl_event
from synapse.types import EventID, RoomID, UserID

Expand Down Expand Up @@ -87,6 +93,29 @@ def validate_new(self, event: EventBase, config: HomeServerConfig):
400, "Can't create an ACL event that denies the local server"
)

if event.type == EventTypes.PowerLevels:
try:
jsonschema.validate(
instance=event.content,
schema=POWER_LEVELS_SCHEMA,
cls=plValidator,
)
except jsonschema.ValidationError as e:
if e.path:
# example: "users_default": '0' is not of type 'integer'
message = '"' + 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,
)

def _validate_retention(self, event: EventBase):
"""Checks that an event that defines the retention policy for a room respects the
format enforced by the spec.
Expand Down Expand Up @@ -185,3 +214,47 @@ def _ensure_strings(self, d, keys):
def _ensure_state_event(self, event):
if not event.is_state():
raise SynapseError(400, "'%s' must be state events" % (event.type,))


POWER_LEVELS_SCHEMA = {
"type": "object",
"properties": {
"ban": {"$ref": "#/definitions/int"},
"events": {"$ref": "#/definitions/objectOfInts"},
"events_default": {"$ref": "#/definitions/int"},
"invite": {"$ref": "#/definitions/int"},
"kick": {"$ref": "#/definitions/int"},
"notifications": {"$ref": "#/definitions/objectOfInts"},
"redact": {"$ref": "#/definitions/int"},
"state_default": {"$ref": "#/definitions/int"},
"users": {"$ref": "#/definitions/objectOfInts"},
"users_default": {"$ref": "#/definitions/int"},
},
"definitions": {
"int": {
"type": "integer",
"minimum": CANONICALJSON_MIN_INT,
"maximum": CANONICALJSON_MAX_INT,
},
"objectOfInts": {
"type": "object",
"additionalProperties": {"$ref": "#/definitions/int"},
},
},
}


def _create_power_level_validator():
validator = jsonschema.validators.validator_for(POWER_LEVELS_SCHEMA)

# by default jsonschema does not consider a frozendict to be an object so
# we need to use a custom type checker
# 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)
)

return jsonschema.validators.extend(validator, type_checker=type_checker)


plValidator = _create_power_level_validator()
3 changes: 2 additions & 1 deletion synapse/python_dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@
# [1] https://pip.pypa.io/en/stable/reference/pip_install/#requirement-specifiers.

REQUIREMENTS = [
"jsonschema>=2.5.1",
# we use the TYPE_CHECKER.redefine method added in jsonschema 3.0.0
"jsonschema>=3.0.0",
richvdh marked this conversation as resolved.
Show resolved Hide resolved
"frozendict>=1",
"unpaddedbase64>=1.1.0",
"canonicaljson>=1.4.0",
Expand Down
78 changes: 78 additions & 0 deletions tests/rest/client/test_power_levels.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from synapse.api.errors import Codes
from synapse.events.utils import CANONICALJSON_MAX_INT, CANONICALJSON_MIN_INT
from synapse.rest import admin
from synapse.rest.client import login, room, sync

Expand Down Expand Up @@ -203,3 +205,79 @@ def test_admins_can_tombstone_room(self):
tok=self.admin_access_token,
expect_code=200, # expect success
)

def test_cannot_set_string_power_levels(self):
room_power_levels = self.helper.get_state(
self.room_id,
"m.room.power_levels",
tok=self.admin_access_token,
)

# Update existing power levels with user at PL "0"
room_power_levels["users"].update({self.user_user_id: "0"})

body = self.helper.send_state(
self.room_id,
"m.room.power_levels",
room_power_levels,
tok=self.admin_access_token,
expect_code=400, # expect failure
)

self.assertEqual(
body["errcode"],
Codes.BAD_JSON,
body,
)

def test_cannot_set_unsafe_large_power_levels(self):
room_power_levels = self.helper.get_state(
self.room_id,
"m.room.power_levels",
tok=self.admin_access_token,
)

# Update existing power levels with user at PL above the max safe integer
room_power_levels["users"].update(
{self.user_user_id: CANONICALJSON_MAX_INT + 1}
)

body = self.helper.send_state(
self.room_id,
"m.room.power_levels",
room_power_levels,
tok=self.admin_access_token,
expect_code=400, # expect failure
)

self.assertEqual(
body["errcode"],
Codes.BAD_JSON,
body,
)

def test_cannot_set_unsafe_small_power_levels(self):
room_power_levels = self.helper.get_state(
self.room_id,
"m.room.power_levels",
tok=self.admin_access_token,
)

# Update existing power levels with user at PL below the minimum safe integer
room_power_levels["users"].update(
{self.user_user_id: CANONICALJSON_MIN_INT - 1}
)

body = self.helper.send_state(
self.room_id,
"m.room.power_levels",
room_power_levels,
tok=self.admin_access_token,
expect_code=400, # expect failure
)

self.assertEqual(
body["errcode"],
Codes.BAD_JSON,
body,
)