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

Commit 40f619e

Browse files
authored
Validate new m.room.power_levels events (#10232)
Signed-off-by: Aaron Raimist <[email protected]>
1 parent ad17fbd commit 40f619e

File tree

5 files changed

+160
-4
lines changed

5 files changed

+160
-4
lines changed

changelog.d/10232.bugfix

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Validate new `m.room.power_levels` events. Contributed by @aaronraimist.

synapse/events/utils.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@
3232
# the literal fields "foo\" and "bar" but will instead be treated as "foo\\.bar"
3333
SPLIT_FIELD_REGEX = re.compile(r"(?<!\\)\.")
3434

35+
CANONICALJSON_MAX_INT = (2 ** 53) - 1
36+
CANONICALJSON_MIN_INT = -CANONICALJSON_MAX_INT
37+
3538

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

511514
elif isinstance(value, float):

synapse/events/validator.py

+75-2
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,22 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14-
14+
import collections.abc
1515
from typing import Union
1616

17+
import jsonschema
18+
1719
from synapse.api.constants import MAX_ALIAS_LENGTH, EventTypes, Membership
1820
from synapse.api.errors import Codes, SynapseError
1921
from synapse.api.room_versions import EventFormatVersions
2022
from synapse.config.homeserver import HomeServerConfig
2123
from synapse.events import EventBase
2224
from synapse.events.builder import EventBuilder
23-
from synapse.events.utils import validate_canonicaljson
25+
from synapse.events.utils import (
26+
CANONICALJSON_MAX_INT,
27+
CANONICALJSON_MIN_INT,
28+
validate_canonicaljson,
29+
)
2430
from synapse.federation.federation_server import server_matches_acl_event
2531
from synapse.types import EventID, RoomID, UserID
2632

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

96+
if event.type == EventTypes.PowerLevels:
97+
try:
98+
jsonschema.validate(
99+
instance=event.content,
100+
schema=POWER_LEVELS_SCHEMA,
101+
cls=plValidator,
102+
)
103+
except jsonschema.ValidationError as e:
104+
if e.path:
105+
# example: "users_default": '0' is not of type 'integer'
106+
message = '"' + e.path[-1] + '": ' + e.message # noqa: B306
107+
# jsonschema.ValidationError.message is a valid attribute
108+
else:
109+
# example: '0' is not of type 'integer'
110+
message = e.message # noqa: B306
111+
# jsonschema.ValidationError.message is a valid attribute
112+
113+
raise SynapseError(
114+
code=400,
115+
msg=message,
116+
errcode=Codes.BAD_JSON,
117+
)
118+
90119
def _validate_retention(self, event: EventBase):
91120
"""Checks that an event that defines the retention policy for a room respects the
92121
format enforced by the spec.
@@ -185,3 +214,47 @@ def _ensure_strings(self, d, keys):
185214
def _ensure_state_event(self, event):
186215
if not event.is_state():
187216
raise SynapseError(400, "'%s' must be state events" % (event.type,))
217+
218+
219+
POWER_LEVELS_SCHEMA = {
220+
"type": "object",
221+
"properties": {
222+
"ban": {"$ref": "#/definitions/int"},
223+
"events": {"$ref": "#/definitions/objectOfInts"},
224+
"events_default": {"$ref": "#/definitions/int"},
225+
"invite": {"$ref": "#/definitions/int"},
226+
"kick": {"$ref": "#/definitions/int"},
227+
"notifications": {"$ref": "#/definitions/objectOfInts"},
228+
"redact": {"$ref": "#/definitions/int"},
229+
"state_default": {"$ref": "#/definitions/int"},
230+
"users": {"$ref": "#/definitions/objectOfInts"},
231+
"users_default": {"$ref": "#/definitions/int"},
232+
},
233+
"definitions": {
234+
"int": {
235+
"type": "integer",
236+
"minimum": CANONICALJSON_MIN_INT,
237+
"maximum": CANONICALJSON_MAX_INT,
238+
},
239+
"objectOfInts": {
240+
"type": "object",
241+
"additionalProperties": {"$ref": "#/definitions/int"},
242+
},
243+
},
244+
}
245+
246+
247+
def _create_power_level_validator():
248+
validator = jsonschema.validators.validator_for(POWER_LEVELS_SCHEMA)
249+
250+
# by default jsonschema does not consider a frozendict to be an object so
251+
# we need to use a custom type checker
252+
# https://python-jsonschema.readthedocs.io/en/stable/validate/?highlight=object#validating-with-additional-types
253+
type_checker = validator.TYPE_CHECKER.redefine(
254+
"object", lambda checker, thing: isinstance(thing, collections.abc.Mapping)
255+
)
256+
257+
return jsonschema.validators.extend(validator, type_checker=type_checker)
258+
259+
260+
plValidator = _create_power_level_validator()

synapse/python_dependencies.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@
4848
# [1] https://pip.pypa.io/en/stable/reference/pip_install/#requirement-specifiers.
4949

5050
REQUIREMENTS = [
51-
"jsonschema>=2.5.1",
51+
# we use the TYPE_CHECKER.redefine method added in jsonschema 3.0.0
52+
"jsonschema>=3.0.0",
5253
"frozendict>=1",
5354
"unpaddedbase64>=1.1.0",
5455
"canonicaljson>=1.4.0",

tests/rest/client/test_power_levels.py

+78
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
from synapse.api.errors import Codes
16+
from synapse.events.utils import CANONICALJSON_MAX_INT, CANONICALJSON_MIN_INT
1517
from synapse.rest import admin
1618
from synapse.rest.client import login, room, sync
1719

@@ -203,3 +205,79 @@ def test_admins_can_tombstone_room(self):
203205
tok=self.admin_access_token,
204206
expect_code=200, # expect success
205207
)
208+
209+
def test_cannot_set_string_power_levels(self):
210+
room_power_levels = self.helper.get_state(
211+
self.room_id,
212+
"m.room.power_levels",
213+
tok=self.admin_access_token,
214+
)
215+
216+
# Update existing power levels with user at PL "0"
217+
room_power_levels["users"].update({self.user_user_id: "0"})
218+
219+
body = self.helper.send_state(
220+
self.room_id,
221+
"m.room.power_levels",
222+
room_power_levels,
223+
tok=self.admin_access_token,
224+
expect_code=400, # expect failure
225+
)
226+
227+
self.assertEqual(
228+
body["errcode"],
229+
Codes.BAD_JSON,
230+
body,
231+
)
232+
233+
def test_cannot_set_unsafe_large_power_levels(self):
234+
room_power_levels = self.helper.get_state(
235+
self.room_id,
236+
"m.room.power_levels",
237+
tok=self.admin_access_token,
238+
)
239+
240+
# Update existing power levels with user at PL above the max safe integer
241+
room_power_levels["users"].update(
242+
{self.user_user_id: CANONICALJSON_MAX_INT + 1}
243+
)
244+
245+
body = self.helper.send_state(
246+
self.room_id,
247+
"m.room.power_levels",
248+
room_power_levels,
249+
tok=self.admin_access_token,
250+
expect_code=400, # expect failure
251+
)
252+
253+
self.assertEqual(
254+
body["errcode"],
255+
Codes.BAD_JSON,
256+
body,
257+
)
258+
259+
def test_cannot_set_unsafe_small_power_levels(self):
260+
room_power_levels = self.helper.get_state(
261+
self.room_id,
262+
"m.room.power_levels",
263+
tok=self.admin_access_token,
264+
)
265+
266+
# Update existing power levels with user at PL below the minimum safe integer
267+
room_power_levels["users"].update(
268+
{self.user_user_id: CANONICALJSON_MIN_INT - 1}
269+
)
270+
271+
body = self.helper.send_state(
272+
self.room_id,
273+
"m.room.power_levels",
274+
room_power_levels,
275+
tok=self.admin_access_token,
276+
expect_code=400, # expect failure
277+
)
278+
279+
self.assertEqual(
280+
body["errcode"],
281+
Codes.BAD_JSON,
282+
body,
283+
)

0 commit comments

Comments
 (0)