Skip to content

Commit acc3fd7

Browse files
Add max_depth constraint to ListField and DictField
1 parent 48fe075 commit acc3fd7

File tree

5 files changed

+386
-5
lines changed

5 files changed

+386
-5
lines changed

docs/api-guide/fields.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -471,12 +471,13 @@ Requires either the `Pillow` package or `PIL` package. The `Pillow` package is
471471

472472
A field class that validates a list of objects.
473473

474-
**Signature**: `ListField(child=<A_FIELD_INSTANCE>, allow_empty=True, min_length=None, max_length=None)`
474+
**Signature**: `ListField(child=<A_FIELD_INSTANCE>, allow_empty=True, min_length=None, max_length=None, max_depth=None)`
475475

476476
* `child` - A field instance that should be used for validating the objects in the list. If this argument is not provided then objects in the list will not be validated.
477477
* `allow_empty` - Designates if empty lists are allowed.
478478
* `min_length` - Validates that the list contains no fewer than this number of elements.
479479
* `max_length` - Validates that the list contains no more than this number of elements.
480+
* `max_depth` - Validates that nesting depth does not exceed this value. This applies to both the field schema depth and the raw input data depth. Depth of 0 permits the field itself but no nesting. Defaults to `None` (no limit).
480481

481482
For example, to validate a list of integers you might use something like the following:
482483

@@ -495,10 +496,11 @@ We can now reuse our custom `StringListField` class throughout our application,
495496

496497
A field class that validates a dictionary of objects. The keys in `DictField` are always assumed to be string values.
497498

498-
**Signature**: `DictField(child=<A_FIELD_INSTANCE>, allow_empty=True)`
499+
**Signature**: `DictField(child=<A_FIELD_INSTANCE>, allow_empty=True, max_depth=None)`
499500

500501
* `child` - A field instance that should be used for validating the values in the dictionary. If this argument is not provided then values in the mapping will not be validated.
501502
* `allow_empty` - Designates if empty dictionaries are allowed.
503+
* `max_depth` - Validates that nesting depth does not exceed this value. This applies to both the field schema depth and the raw input data depth. Depth of 0 permits the field itself but no nesting. Defaults to `None` (no limit).
502504

503505
For example, to create a field that validates a mapping of strings to strings, you would write something like this:
504506

rest_framework/fields.py

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1650,14 +1650,16 @@ class ListField(Field):
16501650
'not_a_list': _('Expected a list of items but got type "{input_type}".'),
16511651
'empty': _('This list may not be empty.'),
16521652
'min_length': _('Ensure this field has at least {min_length} elements.'),
1653-
'max_length': _('Ensure this field has no more than {max_length} elements.')
1653+
'max_length': _('Ensure this field has no more than {max_length} elements.'),
1654+
'max_depth': _('List nesting depth exceeds maximum allowed depth of {max_depth}.')
16541655
}
16551656

16561657
def __init__(self, **kwargs):
16571658
self.child = kwargs.pop('child', copy.deepcopy(self.child))
16581659
self.allow_empty = kwargs.pop('allow_empty', True)
16591660
self.max_length = kwargs.pop('max_length', None)
16601661
self.min_length = kwargs.pop('min_length', None)
1662+
self.max_depth = kwargs.pop('max_depth', None)
16611663

16621664
assert not inspect.isclass(self.child), '`child` has not been instantiated.'
16631665
assert self.child.source is None, (
@@ -1666,6 +1668,8 @@ def __init__(self, **kwargs):
16661668
)
16671669

16681670
super().__init__(**kwargs)
1671+
self._current_depth = 0
1672+
self._root_max_depth = self.max_depth
16691673
self.child.bind(field_name='', parent=self)
16701674
if self.max_length is not None:
16711675
message = lazy_format(self.error_messages['max_length'], max_length=self.max_length)
@@ -1674,6 +1678,37 @@ def __init__(self, **kwargs):
16741678
message = lazy_format(self.error_messages['min_length'], min_length=self.min_length)
16751679
self.validators.append(MinLengthValidator(self.min_length, message=message))
16761680

1681+
def bind(self, field_name, parent):
1682+
super().bind(field_name, parent)
1683+
if hasattr(parent, '_root_max_depth') and parent._root_max_depth is not None:
1684+
self._root_max_depth = parent._root_max_depth
1685+
self._current_depth = parent._current_depth + 1
1686+
self._propagate_depth_to_child()
1687+
1688+
def _propagate_depth_to_child(self):
1689+
if self._root_max_depth is not None and hasattr(self.child, '_root_max_depth'):
1690+
self.child._root_max_depth = self._root_max_depth
1691+
self.child._current_depth = self._current_depth + 1
1692+
if hasattr(self.child, '_propagate_depth_to_child'):
1693+
self.child._propagate_depth_to_child()
1694+
1695+
def _check_data_depth(self, data, current=0):
1696+
if self._root_max_depth is not None:
1697+
if isinstance(data, (list, tuple)):
1698+
for item in data:
1699+
if isinstance(item, (list, tuple, dict)):
1700+
next_depth = current + 1
1701+
if next_depth > self._root_max_depth:
1702+
self.fail('max_depth', max_depth=self._root_max_depth)
1703+
self._check_data_depth(item, next_depth)
1704+
elif isinstance(data, dict):
1705+
for value in data.values():
1706+
if isinstance(value, (list, tuple, dict)):
1707+
next_depth = current + 1
1708+
if next_depth > self._root_max_depth:
1709+
self.fail('max_depth', max_depth=self._root_max_depth)
1710+
self._check_data_depth(value, next_depth)
1711+
16771712
def get_value(self, dictionary):
16781713
if self.field_name not in dictionary:
16791714
if getattr(self.root, 'partial', False):
@@ -1699,6 +1734,9 @@ def to_internal_value(self, data):
16991734
self.fail('not_a_list', input_type=type(data).__name__)
17001735
if not self.allow_empty and len(data) == 0:
17011736
self.fail('empty')
1737+
if self._root_max_depth is not None and self._current_depth > self._root_max_depth:
1738+
self.fail('max_depth', max_depth=self._root_max_depth)
1739+
self._check_data_depth(data, self._current_depth)
17021740
return self.run_child_validation(data)
17031741

17041742
def to_representation(self, data):
@@ -1730,11 +1768,13 @@ class DictField(Field):
17301768
default_error_messages = {
17311769
'not_a_dict': _('Expected a dictionary of items but got type "{input_type}".'),
17321770
'empty': _('This dictionary may not be empty.'),
1771+
'max_depth': _('Dictionary nesting depth exceeds maximum allowed depth of {max_depth}.')
17331772
}
17341773

17351774
def __init__(self, **kwargs):
17361775
self.child = kwargs.pop('child', copy.deepcopy(self.child))
17371776
self.allow_empty = kwargs.pop('allow_empty', True)
1777+
self.max_depth = kwargs.pop('max_depth', None)
17381778

17391779
assert not inspect.isclass(self.child), '`child` has not been instantiated.'
17401780
assert self.child.source is None, (
@@ -1743,8 +1783,41 @@ def __init__(self, **kwargs):
17431783
)
17441784

17451785
super().__init__(**kwargs)
1786+
self._current_depth = 0
1787+
self._root_max_depth = self.max_depth
17461788
self.child.bind(field_name='', parent=self)
17471789

1790+
def bind(self, field_name, parent):
1791+
super().bind(field_name, parent)
1792+
if hasattr(parent, '_root_max_depth') and parent._root_max_depth is not None:
1793+
self._root_max_depth = parent._root_max_depth
1794+
self._current_depth = parent._current_depth + 1
1795+
self._propagate_depth_to_child()
1796+
1797+
def _propagate_depth_to_child(self):
1798+
if self._root_max_depth is not None and hasattr(self.child, '_root_max_depth'):
1799+
self.child._root_max_depth = self._root_max_depth
1800+
self.child._current_depth = self._current_depth + 1
1801+
if hasattr(self.child, '_propagate_depth_to_child'):
1802+
self.child._propagate_depth_to_child()
1803+
1804+
def _check_data_depth(self, data, current=0):
1805+
if self._root_max_depth is not None:
1806+
if isinstance(data, dict):
1807+
for value in data.values():
1808+
if isinstance(value, (list, tuple, dict)):
1809+
next_depth = current + 1
1810+
if next_depth > self._root_max_depth:
1811+
self.fail('max_depth', max_depth=self._root_max_depth)
1812+
self._check_data_depth(value, next_depth)
1813+
elif isinstance(data, (list, tuple)):
1814+
for item in data:
1815+
if isinstance(item, (list, tuple, dict)):
1816+
next_depth = current + 1
1817+
if next_depth > self._root_max_depth:
1818+
self.fail('max_depth', max_depth=self._root_max_depth)
1819+
self._check_data_depth(item, next_depth)
1820+
17481821
def get_value(self, dictionary):
17491822
# We override the default field access in order to support
17501823
# dictionaries in HTML forms.
@@ -1762,7 +1835,9 @@ def to_internal_value(self, data):
17621835
self.fail('not_a_dict', input_type=type(data).__name__)
17631836
if not self.allow_empty and len(data) == 0:
17641837
self.fail('empty')
1765-
1838+
if self._root_max_depth is not None and self._current_depth > self._root_max_depth:
1839+
self.fail('max_depth', max_depth=self._root_max_depth)
1840+
self._check_data_depth(data, self._current_depth)
17661841
return self.run_child_validation(data)
17671842

17681843
def to_representation(self, value):

rest_framework/locale/en/LC_MESSAGES/django.po

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# SOME DESCRIPTIVE TITLE.
22
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
33
# This file is distributed under the same license as the PACKAGE package.
4-
#
4+
#
55
# Translators:
66
msgid ""
77
msgstr ""
@@ -357,6 +357,11 @@ msgstr "Ensure this field has at least {min_length} elements."
357357
msgid "Ensure this field has no more than {max_length} elements."
358358
msgstr "Ensure this field has no more than {max_length} elements."
359359

360+
#: fields.py:1654
361+
#, python-brace-format
362+
msgid "List nesting depth exceeds maximum allowed depth of {max_depth}."
363+
msgstr "List nesting depth exceeds maximum allowed depth of {max_depth}."
364+
360365
#: fields.py:1682
361366
#, python-brace-format
362367
msgid "Expected a dictionary of items but got type \"{input_type}\"."
@@ -366,6 +371,11 @@ msgstr "Expected a dictionary of items but got type \"{input_type}\"."
366371
msgid "This dictionary may not be empty."
367372
msgstr "This dictionary may not be empty."
368373

374+
#: fields.py:1733
375+
#, python-brace-format
376+
msgid "Dictionary nesting depth exceeds maximum allowed depth of {max_depth}."
377+
msgstr "Dictionary nesting depth exceeds maximum allowed depth of {max_depth}."
378+
369379
#: fields.py:1755
370380
msgid "Value must be valid JSON."
371381
msgstr "Value must be valid JSON."

rest_framework/serializers.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,23 @@ def __init__(self, instance=None, data=empty, **kwargs):
119119
self._context = kwargs.pop('context', {})
120120
kwargs.pop('many', None)
121121
super().__init__(**kwargs)
122+
self._current_depth = 0
123+
self._root_max_depth = None
124+
125+
def bind(self, field_name, parent):
126+
super().bind(field_name, parent)
127+
if hasattr(parent, '_root_max_depth') and parent._root_max_depth is not None:
128+
self._root_max_depth = parent._root_max_depth
129+
self._current_depth = parent._current_depth + 1
130+
131+
def _propagate_depth_to_child(self):
132+
if self._root_max_depth is not None and 'fields' in self.__dict__:
133+
for field in self.__dict__['fields'].values():
134+
if hasattr(field, '_root_max_depth'):
135+
field._root_max_depth = self._root_max_depth
136+
field._current_depth = self._current_depth + 1
137+
if hasattr(field, '_propagate_depth_to_child'):
138+
field._propagate_depth_to_child()
122139

123140
def __new__(cls, *args, **kwargs):
124141
# We override this method in order to automatically create
@@ -373,6 +390,7 @@ def fields(self):
373390
fields = BindingDict(self)
374391
for key, value in self.get_fields().items():
375392
fields[key] = value
393+
self._propagate_depth_to_child()
376394
return fields
377395

378396
@property

0 commit comments

Comments
 (0)