티스토리 수익 글 보기

티스토리 수익 글 보기

[1.3.x] Added a default limit to the maximum number of forms in a for… · django/django@d7094bb · GitHub
Skip to content

Commit d7094bb

Browse files
committed
[1.3.x] Added a default limit to the maximum number of forms in a formset.
This is a security fix. Disclosure and advisory coming shortly.
1 parent d3a45e1 commit d7094bb

File tree

4 files changed

+88
19
lines changed

4 files changed

+88
19
lines changed

django/forms/formsets.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
ORDERING_FIELD_NAME = 'ORDER'
1717
DELETION_FIELD_NAME = 'DELETE'
1818

19+
# default maximum number of forms in a formset, to prevent memory exhaustion
20+
DEFAULT_MAX_NUM = 1000
21+
1922
class ManagementForm(Form):
2023
"""
2124
``ManagementForm`` is used to keep track of how many form instances
@@ -104,7 +107,7 @@ def initial_form_count(self):
104107
def _construct_forms(self):
105108
# instantiate all the forms and put them in self.forms
106109
self.forms = []
107-
for i in xrange(self.total_form_count()):
110+
for i in xrange(min(self.total_form_count(), self.absolute_max)):
108111
self.forms.append(self._construct_form(i))
109112

110113
def _construct_form(self, i, **kwargs):
@@ -348,9 +351,14 @@ def as_ul(self):
348351
def formset_factory(form, formset=BaseFormSet, extra=1, can_order=False,
349352
can_delete=False, max_num=None):
350353
"""Return a FormSet for the given form class."""
354+
if max_num is None:
355+
max_num = DEFAULT_MAX_NUM
356+
# hard limit on forms instantiated, to prevent memory-exhaustion attacks
357+
# limit defaults to DEFAULT_MAX_NUM, but developer can increase it via max_num
358+
absolute_max = max(DEFAULT_MAX_NUM, max_num)
351359
attrs = {'form': form, 'extra': extra,
352360
'can_order': can_order, 'can_delete': can_delete,
353-
'max_num': max_num}
361+
'max_num': max_num, 'absolute_max': absolute_max}
354362
return type(form.__name__ + 'FormSet', (formset,), attrs)
355363

356364
def all_valid(formsets):

docs/topics/forms/formsets.txt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,10 @@ If the value of ``max_num`` is greater than the number of existing
102102
objects, up to ``extra`` additional blank forms will be added to the formset,
103103
so long as the total number of forms does not exceed ``max_num``.
104104

105-
A ``max_num`` value of ``None`` (the default) puts no limit on the number of
106-
forms displayed. Please note that the default value of ``max_num`` was changed
105+
A ``max_num`` value of ``None`` (the default) puts a high limit on the number
106+
of forms displayed (1000). In practice this is equivalent to no limit.
107+
108+
Please note that the default value of ``max_num`` was changed
107109
from ``0`` to ``None`` in version 1.2 to allow ``0`` as a valid value.
108110

109111
Formset validation

docs/topics/forms/modelforms.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -691,8 +691,8 @@ so long as the total number of forms does not exceed ``max_num``::
691691

692692
.. versionchanged:: 1.2
693693

694-
A ``max_num`` value of ``None`` (the default) puts no limit on the number of
695-
forms displayed.
694+
A ``max_num`` value of ``None`` (the default) puts a high limit on the number
695+
of forms displayed (1000). In practice this is equivalent to no limit.
696696

697697
Using a model formset in a view
698698
-------------------------------

tests/regressiontests/forms/tests/formsets.py

Lines changed: 72 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# -*- coding: utf-8 -*-
2-
from django.forms import Form, CharField, IntegerField, ValidationError, DateField
2+
from django.forms import Form, CharField, IntegerField, ValidationError, DateField, formsets
33
from django.forms.formsets import formset_factory, BaseFormSet
44
from django.utils.unittest import TestCase
55

@@ -47,7 +47,7 @@ def test_basic_formset(self):
4747
# for adding data. By default, it displays 1 blank form. It can display more,
4848
# but we'll look at how to do so later.
4949
formset = ChoiceFormSet(auto_id=False, prefix='choices')
50-
self.assertEqual(str(formset), """<input type="hidden" name="choices-TOTAL_FORMS" value="1" /><input type="hidden" name="choices-INITIAL_FORMS" value="0" /><input type="hidden" name="choices-MAX_NUM_FORMS" />
50+
self.assertEqual(str(formset), """<input type="hidden" name="choices-TOTAL_FORMS" value="1" /><input type="hidden" name="choices-INITIAL_FORMS" value="0" /><input type="hidden" name="choices-MAX_NUM_FORMS" value="1000" />
5151
<tr><th>Choice:</th><td><input type="text" name="choices-0-choice" /></td></tr>
5252
<tr><th>Votes:</th><td><input type="text" name="choices-0-votes" /></td></tr>""")
5353

@@ -623,8 +623,8 @@ def test_limiting_max_forms(self):
623623
# Limiting the maximum number of forms ########################################
624624
# Base case for max_num.
625625

626-
# When not passed, max_num will take its default value of None, i.e. unlimited
627-
# number of forms, only controlled by the value of the extra parameter.
626+
# When not passed, max_num will take a high default value, leaving the
627+
# number of forms only controlled by the value of the extra parameter.
628628

629629
LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=3)
630630
formset = LimitedFavoriteDrinkFormSet()
@@ -671,8 +671,8 @@ def test_limiting_max_forms(self):
671671
def test_max_num_with_initial_data(self):
672672
# max_num with initial data
673673

674-
# When not passed, max_num will take its default value of None, i.e. unlimited
675-
# number of forms, only controlled by the values of the initial and extra
674+
# When not passed, max_num will take a high default value, leaving the
675+
# number of forms only controlled by the value of the initial and extra
676676
# parameters.
677677

678678
initial = [
@@ -805,6 +805,65 @@ def __iter__(self):
805805
self.assertEqual(str(reverse_formset[1]), str(forms[-2]))
806806
self.assertEqual(len(reverse_formset), len(forms))
807807

808+
def test_hard_limit_on_instantiated_forms(self):
809+
"""A formset has a hard limit on the number of forms instantiated."""
810+
# reduce the default limit of 1000 temporarily for testing
811+
_old_DEFAULT_MAX_NUM = formsets.DEFAULT_MAX_NUM
812+
try:
813+
formsets.DEFAULT_MAX_NUM = 3
814+
ChoiceFormSet = formset_factory(Choice)
815+
# someone fiddles with the mgmt form data...
816+
formset = ChoiceFormSet(
817+
{
818+
'choices-TOTAL_FORMS': '4',
819+
'choices-INITIAL_FORMS': '0',
820+
'choices-MAX_NUM_FORMS': '4',
821+
'choices-0-choice': 'Zero',
822+
'choices-0-votes': '0',
823+
'choices-1-choice': 'One',
824+
'choices-1-votes': '1',
825+
'choices-2-choice': 'Two',
826+
'choices-2-votes': '2',
827+
'choices-3-choice': 'Three',
828+
'choices-3-votes': '3',
829+
},
830+
prefix='choices',
831+
)
832+
# But we still only instantiate 3 forms
833+
self.assertEqual(len(formset.forms), 3)
834+
finally:
835+
formsets.DEFAULT_MAX_NUM = _old_DEFAULT_MAX_NUM
836+
837+
def test_increase_hard_limit(self):
838+
"""Can increase the built-in forms limit via a higher max_num."""
839+
# reduce the default limit of 1000 temporarily for testing
840+
_old_DEFAULT_MAX_NUM = formsets.DEFAULT_MAX_NUM
841+
try:
842+
formsets.DEFAULT_MAX_NUM = 3
843+
# for this form, we want a limit of 4
844+
ChoiceFormSet = formset_factory(Choice, max_num=4)
845+
formset = ChoiceFormSet(
846+
{
847+
'choices-TOTAL_FORMS': '4',
848+
'choices-INITIAL_FORMS': '0',
849+
'choices-MAX_NUM_FORMS': '4',
850+
'choices-0-choice': 'Zero',
851+
'choices-0-votes': '0',
852+
'choices-1-choice': 'One',
853+
'choices-1-votes': '1',
854+
'choices-2-choice': 'Two',
855+
'choices-2-votes': '2',
856+
'choices-3-choice': 'Three',
857+
'choices-3-votes': '3',
858+
},
859+
prefix='choices',
860+
)
861+
# This time four forms are instantiated
862+
self.assertEqual(len(formset.forms), 4)
863+
finally:
864+
formsets.DEFAULT_MAX_NUM = _old_DEFAULT_MAX_NUM
865+
866+
808867
data = {
809868
'choices-TOTAL_FORMS': '1', # the number of forms rendered
810869
'choices-INITIAL_FORMS': '0', # the number of forms with initial data
@@ -900,12 +959,12 @@ def test_empty_forms_are_unbound(self):
900959
# The empty forms should be equal.
901960
self.assertEqual(empty_forms[0].as_p(), empty_forms[1].as_p())
902961

903-
class TestEmptyFormSet(TestCase):
962+
class TestEmptyFormSet(TestCase):
904963
"Test that an empty formset still calls clean()"
905-
def test_empty_formset_is_valid(self):
906-
EmptyFsetWontValidateFormset = formset_factory(FavoriteDrinkForm, extra=0, formset=EmptyFsetWontValidate)
907-
formset = EmptyFsetWontValidateFormset(data={'form-INITIAL_FORMS':'0', 'form-TOTAL_FORMS':'0'},prefix="form")
908-
formset2 = EmptyFsetWontValidateFormset(data={'form-INITIAL_FORMS':'0', 'form-TOTAL_FORMS':'1', 'form-0-name':'bah' },prefix="form")
909-
self.assertFalse(formset.is_valid())
910-
self.assertFalse(formset2.is_valid())
964+
def test_empty_formset_is_valid(self):
965+
EmptyFsetWontValidateFormset = formset_factory(FavoriteDrinkForm, extra=0, formset=EmptyFsetWontValidate)
966+
formset = EmptyFsetWontValidateFormset(data={'form-INITIAL_FORMS':'0', 'form-TOTAL_FORMS':'0'},prefix="form")
967+
formset2 = EmptyFsetWontValidateFormset(data={'form-INITIAL_FORMS':'0', 'form-TOTAL_FORMS':'1', 'form-0-name':'bah' },prefix="form")
968+
self.assertFalse(formset.is_valid())
969+
self.assertFalse(formset2.is_valid())
911970

0 commit comments

Comments
 (0)