Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 4 additions & 2 deletions docs/api-guide/fields.md
Original file line number Diff line number Diff line change
Expand Up @@ -471,12 +471,13 @@ Requires either the `Pillow` package or `PIL` package. The `Pillow` package is

A field class that validates a list of objects.

**Signature**: `ListField(child=<A_FIELD_INSTANCE>, allow_empty=True, min_length=None, max_length=None)`
**Signature**: `ListField(child=<A_FIELD_INSTANCE>, allow_empty=True, min_length=None, max_length=None, max_depth=None)`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think adding a default limit for max_depth would be a good idea? Users could still override it if needed, but having a cushion would help even if they don't set a limit. What’s your take on that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I considered that, but a default limit might break existing setups that legitimately use deep nesting. Keeping it None by default feels safer for backward compatibility. That said, we should definitely highlight it in the docs as a security best practice.


* `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.
* `allow_empty` - Designates if empty lists are allowed.
* `min_length` - Validates that the list contains no fewer than this number of elements.
* `max_length` - Validates that the list contains no more than this number of elements.
* `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).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Imo here we should also add warning about the Dos risk if no max_depth were provided.


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

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

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

**Signature**: `DictField(child=<A_FIELD_INSTANCE>, allow_empty=True)`
**Signature**: `DictField(child=<A_FIELD_INSTANCE>, allow_empty=True, max_depth=None)`

* `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.
* `allow_empty` - Designates if empty dictionaries are allowed.
* `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).

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

Expand Down
79 changes: 77 additions & 2 deletions rest_framework/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -1650,14 +1650,16 @@ class ListField(Field):
'not_a_list': _('Expected a list of items but got type "{input_type}".'),
'empty': _('This list may not be empty.'),
'min_length': _('Ensure this field has at least {min_length} elements.'),
'max_length': _('Ensure this field has no more than {max_length} elements.')
'max_length': _('Ensure this field has no more than {max_length} elements.'),
'max_depth': _('List nesting depth exceeds maximum allowed depth of {max_depth}.')
}

def __init__(self, **kwargs):
self.child = kwargs.pop('child', copy.deepcopy(self.child))
self.allow_empty = kwargs.pop('allow_empty', True)
self.max_length = kwargs.pop('max_length', None)
self.min_length = kwargs.pop('min_length', None)
self.max_depth = kwargs.pop('max_depth', None)

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

super().__init__(**kwargs)
self._current_depth = 0
self._root_max_depth = self.max_depth
self.child.bind(field_name='', parent=self)
if self.max_length is not None:
message = lazy_format(self.error_messages['max_length'], max_length=self.max_length)
Expand All @@ -1674,6 +1678,37 @@ def __init__(self, **kwargs):
message = lazy_format(self.error_messages['min_length'], min_length=self.min_length)
self.validators.append(MinLengthValidator(self.min_length, message=message))

def bind(self, field_name, parent):
super().bind(field_name, parent)
if hasattr(parent, '_root_max_depth') and parent._root_max_depth is not None:
self._root_max_depth = parent._root_max_depth
self._current_depth = parent._current_depth + 1
self._propagate_depth_to_child()

def _propagate_depth_to_child(self):
if self._root_max_depth is not None and hasattr(self.child, '_root_max_depth'):
self.child._root_max_depth = self._root_max_depth
self.child._current_depth = self._current_depth + 1
if hasattr(self.child, '_propagate_depth_to_child'):
self.child._propagate_depth_to_child()

def _check_data_depth(self, data, current=0):
if self._root_max_depth is not None:
if isinstance(data, (list, tuple)):
for item in data:
if isinstance(item, (list, tuple, dict)):
next_depth = current + 1
if next_depth > self._root_max_depth:
self.fail('max_depth', max_depth=self._root_max_depth)
self._check_data_depth(item, next_depth)
elif isinstance(data, dict):
for value in data.values():
if isinstance(value, (list, tuple, dict)):
next_depth = current + 1
if next_depth > self._root_max_depth:
self.fail('max_depth', max_depth=self._root_max_depth)
self._check_data_depth(value, next_depth)

def get_value(self, dictionary):
if self.field_name not in dictionary:
if getattr(self.root, 'partial', False):
Expand All @@ -1699,6 +1734,9 @@ def to_internal_value(self, data):
self.fail('not_a_list', input_type=type(data).__name__)
if not self.allow_empty and len(data) == 0:
self.fail('empty')
if self._root_max_depth is not None and self._current_depth > self._root_max_depth:
self.fail('max_depth', max_depth=self._root_max_depth)
self._check_data_depth(data, self._current_depth)
return self.run_child_validation(data)

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

def __init__(self, **kwargs):
self.child = kwargs.pop('child', copy.deepcopy(self.child))
self.allow_empty = kwargs.pop('allow_empty', True)
self.max_depth = kwargs.pop('max_depth', None)

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

super().__init__(**kwargs)
self._current_depth = 0
self._root_max_depth = self.max_depth
self.child.bind(field_name='', parent=self)

def bind(self, field_name, parent):
super().bind(field_name, parent)
if hasattr(parent, '_root_max_depth') and parent._root_max_depth is not None:
self._root_max_depth = parent._root_max_depth
self._current_depth = parent._current_depth + 1
self._propagate_depth_to_child()

def _propagate_depth_to_child(self):
if self._root_max_depth is not None and hasattr(self.child, '_root_max_depth'):
self.child._root_max_depth = self._root_max_depth
self.child._current_depth = self._current_depth + 1
if hasattr(self.child, '_propagate_depth_to_child'):
self.child._propagate_depth_to_child()

def _check_data_depth(self, data, current=0):
if self._root_max_depth is not None:
if isinstance(data, dict):
for value in data.values():
if isinstance(value, (list, tuple, dict)):
next_depth = current + 1
if next_depth > self._root_max_depth:
self.fail('max_depth', max_depth=self._root_max_depth)
self._check_data_depth(value, next_depth)
elif isinstance(data, (list, tuple)):
for item in data:
if isinstance(item, (list, tuple, dict)):
next_depth = current + 1
if next_depth > self._root_max_depth:
self.fail('max_depth', max_depth=self._root_max_depth)
self._check_data_depth(item, next_depth)

def get_value(self, dictionary):
# We override the default field access in order to support
# dictionaries in HTML forms.
Expand All @@ -1762,7 +1835,9 @@ def to_internal_value(self, data):
self.fail('not_a_dict', input_type=type(data).__name__)
if not self.allow_empty and len(data) == 0:
self.fail('empty')

if self._root_max_depth is not None and self._current_depth > self._root_max_depth:
self.fail('max_depth', max_depth=self._root_max_depth)
self._check_data_depth(data, self._current_depth)
return self.run_child_validation(data)

def to_representation(self, value):
Expand Down
12 changes: 11 additions & 1 deletion rest_framework/locale/en/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
#
#
# Translators:
msgid ""
msgstr ""
Expand Down Expand Up @@ -357,6 +357,11 @@ msgstr "Ensure this field has at least {min_length} elements."
msgid "Ensure this field has no more than {max_length} elements."
msgstr "Ensure this field has no more than {max_length} elements."

#: fields.py:1654
#, python-brace-format
msgid "List nesting depth exceeds maximum allowed depth of {max_depth}."
msgstr "List nesting depth exceeds maximum allowed depth of {max_depth}."

#: fields.py:1682
#, python-brace-format
msgid "Expected a dictionary of items but got type \"{input_type}\"."
Expand All @@ -366,6 +371,11 @@ msgstr "Expected a dictionary of items but got type \"{input_type}\"."
msgid "This dictionary may not be empty."
msgstr "This dictionary may not be empty."

#: fields.py:1733
#, python-brace-format
msgid "Dictionary nesting depth exceeds maximum allowed depth of {max_depth}."
msgstr "Dictionary nesting depth exceeds maximum allowed depth of {max_depth}."

#: fields.py:1755
msgid "Value must be valid JSON."
msgstr "Value must be valid JSON."
Expand Down
18 changes: 18 additions & 0 deletions rest_framework/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,23 @@ def __init__(self, instance=None, data=empty, **kwargs):
self._context = kwargs.pop('context', {})
kwargs.pop('many', None)
super().__init__(**kwargs)
self._current_depth = 0
self._root_max_depth = None

def bind(self, field_name, parent):
super().bind(field_name, parent)
if hasattr(parent, '_root_max_depth') and parent._root_max_depth is not None:
self._root_max_depth = parent._root_max_depth
self._current_depth = parent._current_depth + 1

def _propagate_depth_to_child(self):
if self._root_max_depth is not None and 'fields' in self.__dict__:
for field in self.__dict__['fields'].values():
if hasattr(field, '_root_max_depth'):
field._root_max_depth = self._root_max_depth
field._current_depth = self._current_depth + 1
if hasattr(field, '_propagate_depth_to_child'):
field._propagate_depth_to_child()

def __new__(cls, *args, **kwargs):
# We override this method in order to automatically create
Expand Down Expand Up @@ -373,6 +390,7 @@ def fields(self):
fields = BindingDict(self)
for key, value in self.get_fields().items():
fields[key] = value
self._propagate_depth_to_child()
return fields

@property
Expand Down
Loading
Loading