Validate new m.room.power_levels events (#10232)

Signed-off-by: Aaron Raimist <aaron@raim.ist>
This commit is contained in:
Aaron Raimist 2021-08-26 11:07:58 -05:00 committed by GitHub
parent ad17fbd20e
commit 40f619eaa5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 160 additions and 4 deletions

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

@ -0,0 +1 @@
Validate new `m.room.power_levels` events. Contributed by @aaronraimist.

View File

@ -32,6 +32,9 @@ from . import EventBase
# the literal fields "foo\" and "bar" but will instead be treated as "foo\\.bar" # the literal fields "foo\" and "bar" but will instead be treated as "foo\\.bar"
SPLIT_FIELD_REGEX = re.compile(r"(?<!\\)\.") SPLIT_FIELD_REGEX = re.compile(r"(?<!\\)\.")
CANONICALJSON_MAX_INT = (2 ** 53) - 1
CANONICALJSON_MIN_INT = -CANONICALJSON_MAX_INT
def prune_event(event: EventBase) -> EventBase: def prune_event(event: EventBase) -> EventBase:
"""Returns a pruned version of the given event, which removes all keys we """Returns a pruned version of the given event, which removes all keys we
@ -505,7 +508,7 @@ def validate_canonicaljson(value: Any):
* NaN, Infinity, -Infinity * NaN, Infinity, -Infinity
""" """
if isinstance(value, int): 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) raise SynapseError(400, "JSON integer out of range", Codes.BAD_JSON)
elif isinstance(value, float): elif isinstance(value, float):

View File

@ -11,16 +11,22 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# 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 collections.abc
from typing import Union from typing import Union
import jsonschema
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.config.homeserver import HomeServerConfig from synapse.config.homeserver import HomeServerConfig
from synapse.events import EventBase from synapse.events import EventBase
from synapse.events.builder import EventBuilder 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.federation.federation_server import server_matches_acl_event
from synapse.types import EventID, RoomID, UserID from synapse.types import EventID, RoomID, UserID
@ -87,6 +93,29 @@ class EventValidator:
400, "Can't create an ACL event that denies the local server" 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): def _validate_retention(self, event: EventBase):
"""Checks that an event that defines the retention policy for a room respects the """Checks that an event that defines the retention policy for a room respects the
format enforced by the spec. format enforced by the spec.
@ -185,3 +214,47 @@ class EventValidator:
def _ensure_state_event(self, event): def _ensure_state_event(self, event):
if not event.is_state(): if not event.is_state():
raise SynapseError(400, "'%s' must be state events" % (event.type,)) 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()

View File

@ -48,7 +48,8 @@ logger = logging.getLogger(__name__)
# [1] https://pip.pypa.io/en/stable/reference/pip_install/#requirement-specifiers. # [1] https://pip.pypa.io/en/stable/reference/pip_install/#requirement-specifiers.
REQUIREMENTS = [ REQUIREMENTS = [
"jsonschema>=2.5.1", # we use the TYPE_CHECKER.redefine method added in jsonschema 3.0.0
"jsonschema>=3.0.0",
"frozendict>=1", "frozendict>=1",
"unpaddedbase64>=1.1.0", "unpaddedbase64>=1.1.0",
"canonicaljson>=1.4.0", "canonicaljson>=1.4.0",

View File

@ -12,6 +12,8 @@
# 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.
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 import admin
from synapse.rest.client import login, room, sync from synapse.rest.client import login, room, sync
@ -203,3 +205,79 @@ class PowerLevelsTestCase(HomeserverTestCase):
tok=self.admin_access_token, tok=self.admin_access_token,
expect_code=200, # expect success 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,
)