mirror of https://github.com/django/django.git
Fixed #20347 -- Allowed customizing the maximum number of instantiated forms in formsets.
Co-authored-by: ethurgood <ethurgood@gmail.com>
This commit is contained in:
parent
b5aa9cb20f
commit
433dd737f9
|
@ -433,16 +433,21 @@ class BaseFormSet:
|
||||||
|
|
||||||
def formset_factory(form, formset=BaseFormSet, extra=1, can_order=False,
|
def formset_factory(form, formset=BaseFormSet, extra=1, can_order=False,
|
||||||
can_delete=False, max_num=None, validate_max=False,
|
can_delete=False, max_num=None, validate_max=False,
|
||||||
min_num=None, validate_min=False):
|
min_num=None, validate_min=False, absolute_max=None):
|
||||||
"""Return a FormSet for the given form class."""
|
"""Return a FormSet for the given form class."""
|
||||||
if min_num is None:
|
if min_num is None:
|
||||||
min_num = DEFAULT_MIN_NUM
|
min_num = DEFAULT_MIN_NUM
|
||||||
if max_num is None:
|
if max_num is None:
|
||||||
max_num = DEFAULT_MAX_NUM
|
max_num = DEFAULT_MAX_NUM
|
||||||
# hard limit on forms instantiated, to prevent memory-exhaustion attacks
|
# absolute_max is a hard limit on forms instantiated, to prevent
|
||||||
# limit is simply max_num + DEFAULT_MAX_NUM (which is 2*DEFAULT_MAX_NUM
|
# memory-exhaustion attacks. Default to max_num + DEFAULT_MAX_NUM
|
||||||
# if max_num is None in the first place)
|
# (which is 2 * DEFAULT_MAX_NUM if max_num is None in the first place).
|
||||||
absolute_max = max_num + DEFAULT_MAX_NUM
|
if absolute_max is None:
|
||||||
|
absolute_max = max_num + DEFAULT_MAX_NUM
|
||||||
|
if max_num > absolute_max:
|
||||||
|
raise ValueError(
|
||||||
|
"'absolute_max' must be greater or equal to 'max_num'."
|
||||||
|
)
|
||||||
attrs = {
|
attrs = {
|
||||||
'form': form,
|
'form': form,
|
||||||
'extra': extra,
|
'extra': extra,
|
||||||
|
|
|
@ -862,7 +862,8 @@ def modelformset_factory(model, form=ModelForm, formfield_callback=None,
|
||||||
can_order=False, max_num=None, fields=None, exclude=None,
|
can_order=False, max_num=None, fields=None, exclude=None,
|
||||||
widgets=None, validate_max=False, localized_fields=None,
|
widgets=None, validate_max=False, localized_fields=None,
|
||||||
labels=None, help_texts=None, error_messages=None,
|
labels=None, help_texts=None, error_messages=None,
|
||||||
min_num=None, validate_min=False, field_classes=None):
|
min_num=None, validate_min=False, field_classes=None,
|
||||||
|
absolute_max=None):
|
||||||
"""Return a FormSet class for the given Django model class."""
|
"""Return a FormSet class for the given Django model class."""
|
||||||
meta = getattr(form, 'Meta', None)
|
meta = getattr(form, 'Meta', None)
|
||||||
if (getattr(meta, 'fields', fields) is None and
|
if (getattr(meta, 'fields', fields) is None and
|
||||||
|
@ -879,7 +880,8 @@ def modelformset_factory(model, form=ModelForm, formfield_callback=None,
|
||||||
error_messages=error_messages, field_classes=field_classes)
|
error_messages=error_messages, field_classes=field_classes)
|
||||||
FormSet = formset_factory(form, formset, extra=extra, min_num=min_num, max_num=max_num,
|
FormSet = formset_factory(form, formset, extra=extra, min_num=min_num, max_num=max_num,
|
||||||
can_order=can_order, can_delete=can_delete,
|
can_order=can_order, can_delete=can_delete,
|
||||||
validate_min=validate_min, validate_max=validate_max)
|
validate_min=validate_min, validate_max=validate_max,
|
||||||
|
absolute_max=absolute_max)
|
||||||
FormSet.model = model
|
FormSet.model = model
|
||||||
return FormSet
|
return FormSet
|
||||||
|
|
||||||
|
@ -1048,7 +1050,8 @@ def inlineformset_factory(parent_model, model, form=ModelForm,
|
||||||
can_delete=True, max_num=None, formfield_callback=None,
|
can_delete=True, max_num=None, formfield_callback=None,
|
||||||
widgets=None, validate_max=False, localized_fields=None,
|
widgets=None, validate_max=False, localized_fields=None,
|
||||||
labels=None, help_texts=None, error_messages=None,
|
labels=None, help_texts=None, error_messages=None,
|
||||||
min_num=None, validate_min=False, field_classes=None):
|
min_num=None, validate_min=False, field_classes=None,
|
||||||
|
absolute_max=None):
|
||||||
"""
|
"""
|
||||||
Return an ``InlineFormSet`` for the given kwargs.
|
Return an ``InlineFormSet`` for the given kwargs.
|
||||||
|
|
||||||
|
@ -1078,6 +1081,7 @@ def inlineformset_factory(parent_model, model, form=ModelForm,
|
||||||
'help_texts': help_texts,
|
'help_texts': help_texts,
|
||||||
'error_messages': error_messages,
|
'error_messages': error_messages,
|
||||||
'field_classes': field_classes,
|
'field_classes': field_classes,
|
||||||
|
'absolute_max': absolute_max,
|
||||||
}
|
}
|
||||||
FormSet = modelformset_factory(model, **kwargs)
|
FormSet = modelformset_factory(model, **kwargs)
|
||||||
FormSet.fk = fk
|
FormSet.fk = fk
|
||||||
|
|
|
@ -11,8 +11,12 @@ Formset API reference. For introductory material about formsets, see the
|
||||||
``formset_factory``
|
``formset_factory``
|
||||||
===================
|
===================
|
||||||
|
|
||||||
.. function:: formset_factory(form, formset=BaseFormSet, extra=1, can_order=False, can_delete=False, max_num=None, validate_max=False, min_num=None, validate_min=False)
|
.. function:: formset_factory(form, formset=BaseFormSet, extra=1, can_order=False, can_delete=False, max_num=None, validate_max=False, min_num=None, validate_min=False, absolute_max=None)
|
||||||
|
|
||||||
Returns a ``FormSet`` class for the given ``form`` class.
|
Returns a ``FormSet`` class for the given ``form`` class.
|
||||||
|
|
||||||
See :doc:`formsets </topics/forms/formsets>` for example usage.
|
See :doc:`formsets </topics/forms/formsets>` for example usage.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.2
|
||||||
|
|
||||||
|
The ``absolute_max`` argument was added.
|
||||||
|
|
|
@ -52,7 +52,7 @@ Model Form API reference. For introductory material about model forms, see the
|
||||||
``modelformset_factory``
|
``modelformset_factory``
|
||||||
========================
|
========================
|
||||||
|
|
||||||
.. function:: modelformset_factory(model, form=ModelForm, formfield_callback=None, formset=BaseModelFormSet, extra=1, can_delete=False, can_order=False, max_num=None, fields=None, exclude=None, widgets=None, validate_max=False, localized_fields=None, labels=None, help_texts=None, error_messages=None, min_num=None, validate_min=False, field_classes=None)
|
.. function:: modelformset_factory(model, form=ModelForm, formfield_callback=None, formset=BaseModelFormSet, extra=1, can_delete=False, can_order=False, max_num=None, fields=None, exclude=None, widgets=None, validate_max=False, localized_fields=None, labels=None, help_texts=None, error_messages=None, min_num=None, validate_min=False, field_classes=None, absolute_max=None)
|
||||||
|
|
||||||
Returns a ``FormSet`` class for the given ``model`` class.
|
Returns a ``FormSet`` class for the given ``model`` class.
|
||||||
|
|
||||||
|
@ -62,16 +62,20 @@ Model Form API reference. For introductory material about model forms, see the
|
||||||
through to :func:`~django.forms.models.modelform_factory`.
|
through to :func:`~django.forms.models.modelform_factory`.
|
||||||
|
|
||||||
Arguments ``formset``, ``extra``, ``max_num``, ``can_order``,
|
Arguments ``formset``, ``extra``, ``max_num``, ``can_order``,
|
||||||
``can_delete`` and ``validate_max`` are passed through to
|
``can_delete``, ``validate_max``, and ``absolute_max`` are passed through
|
||||||
:func:`~django.forms.formsets.formset_factory`. See :doc:`formsets
|
to :func:`~django.forms.formsets.formset_factory`. See :doc:`formsets
|
||||||
</topics/forms/formsets>` for details.
|
</topics/forms/formsets>` for details.
|
||||||
|
|
||||||
See :ref:`model-formsets` for example usage.
|
See :ref:`model-formsets` for example usage.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.2
|
||||||
|
|
||||||
|
The ``absolute_max`` argument was added.
|
||||||
|
|
||||||
``inlineformset_factory``
|
``inlineformset_factory``
|
||||||
=========================
|
=========================
|
||||||
|
|
||||||
.. function:: inlineformset_factory(parent_model, model, form=ModelForm, formset=BaseInlineFormSet, fk_name=None, fields=None, exclude=None, extra=3, can_order=False, can_delete=True, max_num=None, formfield_callback=None, widgets=None, validate_max=False, localized_fields=None, labels=None, help_texts=None, error_messages=None, min_num=None, validate_min=False, field_classes=None)
|
.. function:: inlineformset_factory(parent_model, model, form=ModelForm, formset=BaseInlineFormSet, fk_name=None, fields=None, exclude=None, extra=3, can_order=False, can_delete=True, max_num=None, formfield_callback=None, widgets=None, validate_max=False, localized_fields=None, labels=None, help_texts=None, error_messages=None, min_num=None, validate_min=False, field_classes=None, absolute_max=None)
|
||||||
|
|
||||||
Returns an ``InlineFormSet`` using :func:`modelformset_factory` with
|
Returns an ``InlineFormSet`` using :func:`modelformset_factory` with
|
||||||
defaults of ``formset=``:class:`~django.forms.models.BaseInlineFormSet`,
|
defaults of ``formset=``:class:`~django.forms.models.BaseInlineFormSet`,
|
||||||
|
@ -81,3 +85,7 @@ Model Form API reference. For introductory material about model forms, see the
|
||||||
the ``parent_model``, you must specify a ``fk_name``.
|
the ``parent_model``, you must specify a ``fk_name``.
|
||||||
|
|
||||||
See :ref:`inline-formsets` for example usage.
|
See :ref:`inline-formsets` for example usage.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.2
|
||||||
|
|
||||||
|
The ``absolute_max`` argument was added.
|
||||||
|
|
|
@ -136,7 +136,10 @@ File Uploads
|
||||||
Forms
|
Forms
|
||||||
~~~~~
|
~~~~~
|
||||||
|
|
||||||
* ...
|
* The new ``absolute_max`` argument for :func:`.formset_factory`,
|
||||||
|
:func:`.inlineformset_factory`, and :func:`.modelformset_factory` allows
|
||||||
|
customizing the maximum number of forms that can be instantiated when
|
||||||
|
supplying ``POST`` data. See :ref:`formsets-absolute-max` for more details.
|
||||||
|
|
||||||
Generic Views
|
Generic Views
|
||||||
~~~~~~~~~~~~~
|
~~~~~~~~~~~~~
|
||||||
|
|
|
@ -126,6 +126,38 @@ affect validation. If ``validate_max=True`` is passed to the
|
||||||
:func:`~django.forms.formsets.formset_factory`, then ``max_num`` will affect
|
:func:`~django.forms.formsets.formset_factory`, then ``max_num`` will affect
|
||||||
validation. See :ref:`validate_max`.
|
validation. See :ref:`validate_max`.
|
||||||
|
|
||||||
|
.. _formsets-absolute-max:
|
||||||
|
|
||||||
|
Limiting the maximum number of instantiated forms
|
||||||
|
=================================================
|
||||||
|
|
||||||
|
.. versionadded:: 3.2
|
||||||
|
|
||||||
|
The ``absolute_max`` parameter to :func:`.formset_factory` allows limiting the
|
||||||
|
number of forms that can be instantiated when supplying ``POST`` data. This
|
||||||
|
protects against memory exhaustion attacks using forged ``POST`` requests::
|
||||||
|
|
||||||
|
>>> from django.forms.formsets import formset_factory
|
||||||
|
>>> from myapp.forms import ArticleForm
|
||||||
|
>>> ArticleFormSet = formset_factory(ArticleForm, absolute_max=1500)
|
||||||
|
>>> data = {
|
||||||
|
... 'form-TOTAL_FORMS': '1501',
|
||||||
|
... 'form-INITIAL_FORMS': '0',
|
||||||
|
... 'form-MAX_NUM_FORMS': '',
|
||||||
|
... }
|
||||||
|
>>> formset = ArticleFormSet(data)
|
||||||
|
>>> len(formset.forms)
|
||||||
|
1500
|
||||||
|
>>> formset.is_valid()
|
||||||
|
False
|
||||||
|
>>> formset.non_form_errors()
|
||||||
|
['Please submit 1000 or fewer forms.']
|
||||||
|
|
||||||
|
When ``absolute_max`` is None, it defaults to ``max_num + 1000``. (If
|
||||||
|
``max_num`` is ``None``, it defaults to ``2000``).
|
||||||
|
|
||||||
|
If ``absolute_max`` is less than ``max_num``, a ``ValueError`` will be raised.
|
||||||
|
|
||||||
Formset validation
|
Formset validation
|
||||||
==================
|
==================
|
||||||
|
|
||||||
|
@ -348,11 +380,11 @@ excessive.
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
Regardless of ``validate_max``, if the number of forms in a data set
|
Regardless of ``validate_max``, if the number of forms in a data set
|
||||||
exceeds ``max_num`` by more than 1000, then the form will fail to validate
|
exceeds ``absolute_max``, then the form will fail to validate as if
|
||||||
as if ``validate_max`` were set, and additionally only the first 1000
|
``validate_max`` were set, and additionally only the first ``absolute_max``
|
||||||
forms above ``max_num`` will be validated. The remainder will be
|
forms will be validated. The remainder will be truncated entirely. This is
|
||||||
truncated entirely. This is to protect against memory exhaustion attacks
|
to protect against memory exhaustion attacks using forged POST requests.
|
||||||
using forged POST requests.
|
See :ref:`formsets-absolute-max`.
|
||||||
|
|
||||||
``validate_min``
|
``validate_min``
|
||||||
----------------
|
----------------
|
||||||
|
|
|
@ -892,6 +892,55 @@ class FormsFormsetTestCase(SimpleTestCase):
|
||||||
)
|
)
|
||||||
self.assertEqual(formset.absolute_max, 2000)
|
self.assertEqual(formset.absolute_max, 2000)
|
||||||
|
|
||||||
|
def test_absolute_max(self):
|
||||||
|
data = {
|
||||||
|
'form-TOTAL_FORMS': '2001',
|
||||||
|
'form-INITIAL_FORMS': '0',
|
||||||
|
'form-MAX_NUM_FORMS': '0',
|
||||||
|
}
|
||||||
|
AbsoluteMaxFavoriteDrinksFormSet = formset_factory(
|
||||||
|
FavoriteDrinkForm,
|
||||||
|
absolute_max=3000,
|
||||||
|
)
|
||||||
|
formset = AbsoluteMaxFavoriteDrinksFormSet(data=data)
|
||||||
|
self.assertIs(formset.is_valid(), True)
|
||||||
|
self.assertEqual(len(formset.forms), 2001)
|
||||||
|
# absolute_max provides a hard limit.
|
||||||
|
data['form-TOTAL_FORMS'] = '3001'
|
||||||
|
formset = AbsoluteMaxFavoriteDrinksFormSet(data=data)
|
||||||
|
self.assertIs(formset.is_valid(), False)
|
||||||
|
self.assertEqual(len(formset.forms), 3000)
|
||||||
|
self.assertEqual(
|
||||||
|
formset.non_form_errors(),
|
||||||
|
['Please submit 1000 or fewer forms.'],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_absolute_max_with_max_num(self):
|
||||||
|
data = {
|
||||||
|
'form-TOTAL_FORMS': '1001',
|
||||||
|
'form-INITIAL_FORMS': '0',
|
||||||
|
'form-MAX_NUM_FORMS': '0',
|
||||||
|
}
|
||||||
|
LimitedFavoriteDrinksFormSet = formset_factory(
|
||||||
|
FavoriteDrinkForm,
|
||||||
|
max_num=30,
|
||||||
|
absolute_max=1000,
|
||||||
|
)
|
||||||
|
formset = LimitedFavoriteDrinksFormSet(data=data)
|
||||||
|
self.assertIs(formset.is_valid(), False)
|
||||||
|
self.assertEqual(len(formset.forms), 1000)
|
||||||
|
self.assertEqual(
|
||||||
|
formset.non_form_errors(),
|
||||||
|
['Please submit 30 or fewer forms.'],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_absolute_max_invalid(self):
|
||||||
|
msg = "'absolute_max' must be greater or equal to 'max_num'."
|
||||||
|
for max_num in [None, 31]:
|
||||||
|
with self.subTest(max_num=max_num):
|
||||||
|
with self.assertRaisesMessage(ValueError, msg):
|
||||||
|
formset_factory(FavoriteDrinkForm, max_num=max_num, absolute_max=30)
|
||||||
|
|
||||||
def test_more_initial_form_result_in_one(self):
|
def test_more_initial_form_result_in_one(self):
|
||||||
"""
|
"""
|
||||||
One form from initial and extra=3 with max_num=2 results in the one
|
One form from initial and extra=3 with max_num=2 results in the one
|
||||||
|
|
|
@ -1838,3 +1838,81 @@ class TestModelFormsetOverridesTroughFormMeta(TestCase):
|
||||||
form = BookFormSet.form(data={'title': 'Foo ' * 30, 'author': author.id})
|
form = BookFormSet.form(data={'title': 'Foo ' * 30, 'author': author.id})
|
||||||
self.assertIs(Book._meta.get_field('title').__class__, models.CharField)
|
self.assertIs(Book._meta.get_field('title').__class__, models.CharField)
|
||||||
self.assertIsInstance(form.fields['title'], forms.SlugField)
|
self.assertIsInstance(form.fields['title'], forms.SlugField)
|
||||||
|
|
||||||
|
def test_modelformset_factory_absolute_max(self):
|
||||||
|
AuthorFormSet = modelformset_factory(Author, fields='__all__', absolute_max=1500)
|
||||||
|
data = {
|
||||||
|
'form-TOTAL_FORMS': '1501',
|
||||||
|
'form-INITIAL_FORMS': '0',
|
||||||
|
'form-MAX_NUM_FORMS': '0',
|
||||||
|
}
|
||||||
|
formset = AuthorFormSet(data=data)
|
||||||
|
self.assertIs(formset.is_valid(), False)
|
||||||
|
self.assertEqual(len(formset.forms), 1500)
|
||||||
|
self.assertEqual(
|
||||||
|
formset.non_form_errors(),
|
||||||
|
['Please submit 1000 or fewer forms.'],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_modelformset_factory_absolute_max_with_max_num(self):
|
||||||
|
AuthorFormSet = modelformset_factory(
|
||||||
|
Author,
|
||||||
|
fields='__all__',
|
||||||
|
max_num=20,
|
||||||
|
absolute_max=100,
|
||||||
|
)
|
||||||
|
data = {
|
||||||
|
'form-TOTAL_FORMS': '101',
|
||||||
|
'form-INITIAL_FORMS': '0',
|
||||||
|
'form-MAX_NUM_FORMS': '0',
|
||||||
|
}
|
||||||
|
formset = AuthorFormSet(data=data)
|
||||||
|
self.assertIs(formset.is_valid(), False)
|
||||||
|
self.assertEqual(len(formset.forms), 100)
|
||||||
|
self.assertEqual(
|
||||||
|
formset.non_form_errors(),
|
||||||
|
['Please submit 20 or fewer forms.'],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_inlineformset_factory_absolute_max(self):
|
||||||
|
author = Author.objects.create(name='Charles Baudelaire')
|
||||||
|
BookFormSet = inlineformset_factory(
|
||||||
|
Author,
|
||||||
|
Book,
|
||||||
|
fields='__all__',
|
||||||
|
absolute_max=1500,
|
||||||
|
)
|
||||||
|
data = {
|
||||||
|
'book_set-TOTAL_FORMS': '1501',
|
||||||
|
'book_set-INITIAL_FORMS': '0',
|
||||||
|
'book_set-MAX_NUM_FORMS': '0',
|
||||||
|
}
|
||||||
|
formset = BookFormSet(data, instance=author)
|
||||||
|
self.assertIs(formset.is_valid(), False)
|
||||||
|
self.assertEqual(len(formset.forms), 1500)
|
||||||
|
self.assertEqual(
|
||||||
|
formset.non_form_errors(),
|
||||||
|
['Please submit 1000 or fewer forms.'],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_inlineformset_factory_absolute_max_with_max_num(self):
|
||||||
|
author = Author.objects.create(name='Charles Baudelaire')
|
||||||
|
BookFormSet = inlineformset_factory(
|
||||||
|
Author,
|
||||||
|
Book,
|
||||||
|
fields='__all__',
|
||||||
|
max_num=20,
|
||||||
|
absolute_max=100,
|
||||||
|
)
|
||||||
|
data = {
|
||||||
|
'book_set-TOTAL_FORMS': '101',
|
||||||
|
'book_set-INITIAL_FORMS': '0',
|
||||||
|
'book_set-MAX_NUM_FORMS': '0',
|
||||||
|
}
|
||||||
|
formset = BookFormSet(data, instance=author)
|
||||||
|
self.assertIs(formset.is_valid(), False)
|
||||||
|
self.assertEqual(len(formset.forms), 100)
|
||||||
|
self.assertEqual(
|
||||||
|
formset.non_form_errors(),
|
||||||
|
['Please submit 20 or fewer forms.'],
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in New Issue