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

Commit

Permalink
Validate new m.room.power_levels events (#10232)
Browse files Browse the repository at this point in the history
Signed-off-by: Aaron Raimist <aaron@raim.ist>
  • Loading branch information
aaronraimist authored Aug 26, 2021
1 parent ad17fbd commit 40f619e
Show file tree
Hide file tree
Showing 5 changed files with 160 additions and 4 deletions.
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",
"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,
)

0 comments on commit 40f619e

Please sign in to comment.