Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 0cc350a

Browse files
aaugustincarljm
authored andcommitted
[1.4.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 0e7861a commit 0cc350a

File tree

5 files changed

+82
-13
lines changed

5 files changed

+82
-13
lines changed

django/forms/formsets.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
ORDERING_FIELD_NAME = 'ORDER'
2020
DELETION_FIELD_NAME = 'DELETE'
2121

22+
# default maximum number of forms in a formset, to prevent memory exhaustion
23+
DEFAULT_MAX_NUM = 1000
24+
2225
class ManagementForm(Form):
2326
"""
2427
``ManagementForm`` is used to keep track of how many form instances
@@ -111,7 +114,7 @@ def initial_form_count(self):
111114
def _construct_forms(self):
112115
# instantiate all the forms and put them in self.forms
113116
self.forms = []
114-
for i in xrange(self.total_form_count()):
117+
for i in xrange(min(self.total_form_count(), self.absolute_max)):
115118
self.forms.append(self._construct_form(i))
116119

117120
def _construct_form(self, i, **kwargs):
@@ -360,9 +363,14 @@ def as_ul(self):
360363
def formset_factory(form, formset=BaseFormSet, extra=1, can_order=False,
361364
can_delete=False, max_num=None):
362365
"""Return a FormSet for the given form class."""
366+
if max_num is None:
367+
max_num = DEFAULT_MAX_NUM
368+
# hard limit on forms instantiated, to prevent memory-exhaustion attacks
369+
# limit defaults to DEFAULT_MAX_NUM, but developer can increase it via max_num
370+
absolute_max = max(DEFAULT_MAX_NUM, max_num)
363371
attrs = {'form': form, 'extra': extra,
364372
'can_order': can_order, 'can_delete': can_delete,
365-
'max_num': max_num}
373+
'max_num': max_num, 'absolute_max': absolute_max}
366374
return type(form.__name__ + 'FormSet', (formset,), attrs)
367375

368376
def all_valid(formsets):

docs/topics/forms/formsets.txt

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

111-
A ``max_num`` value of ``None`` (the default) puts no limit on the number of
112-
forms displayed. Please note that the default value of ``max_num`` was changed
111+
A ``max_num`` value of ``None`` (the default) puts a high limit on the number
112+
of forms displayed (1000). In practice this is equivalent to no limit.
113+
114+
Please note that the default value of ``max_num`` was changed
113115
from ``0`` to ``None`` in version 1.2 to allow ``0`` as a valid value.
114116

115117
Formset validation

docs/topics/forms/modelforms.txt

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

704704
.. versionchanged:: 1.2
705705

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

709709
Using a model formset in a view
710710
-------------------------------

tests/regressiontests/forms/tests/formsets.py

Lines changed: 64 additions & 6 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.test 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.assertHTMLEqual(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.assertHTMLEqual(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

@@ -650,8 +650,8 @@ def test_limiting_max_forms(self):
650650
# Limiting the maximum number of forms ########################################
651651
# Base case for max_num.
652652

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

656656
LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=3)
657657
formset = LimitedFavoriteDrinkFormSet()
@@ -698,8 +698,8 @@ def test_limiting_max_forms(self):
698698
def test_max_num_with_initial_data(self):
699699
# max_num with initial data
700700

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

705705
initial = [
@@ -844,6 +844,64 @@ def test_formset_nonzero(self):
844844
self.assertEqual(len(formset.forms), 0)
845845
self.assertTrue(formset)
846846

847+
def test_hard_limit_on_instantiated_forms(self):
848+
"""A formset has a hard limit on the number of forms instantiated."""
849+
# reduce the default limit of 1000 temporarily for testing
850+
_old_DEFAULT_MAX_NUM = formsets.DEFAULT_MAX_NUM
851+
try:
852+
formsets.DEFAULT_MAX_NUM = 3
853+
ChoiceFormSet = formset_factory(Choice)
854+
# someone fiddles with the mgmt form data...
855+
formset = ChoiceFormSet(
856+
{
857+
'choices-TOTAL_FORMS': '4',
858+
'choices-INITIAL_FORMS': '0',
859+
'choices-MAX_NUM_FORMS': '4',
860+
'choices-0-choice': 'Zero',
861+
'choices-0-votes': '0',
862+
'choices-1-choice': 'One',
863+
'choices-1-votes': '1',
864+
'choices-2-choice': 'Two',
865+
'choices-2-votes': '2',
866+
'choices-3-choice': 'Three',
867+
'choices-3-votes': '3',
868+
},
869+
prefix='choices',
870+
)
871+
# But we still only instantiate 3 forms
872+
self.assertEqual(len(formset.forms), 3)
873+
finally:
874+
formsets.DEFAULT_MAX_NUM = _old_DEFAULT_MAX_NUM
875+
876+
def test_increase_hard_limit(self):
877+
"""Can increase the built-in forms limit via a higher max_num."""
878+
# reduce the default limit of 1000 temporarily for testing
879+
_old_DEFAULT_MAX_NUM = formsets.DEFAULT_MAX_NUM
880+
try:
881+
formsets.DEFAULT_MAX_NUM = 3
882+
# for this form, we want a limit of 4
883+
ChoiceFormSet = formset_factory(Choice, max_num=4)
884+
formset = ChoiceFormSet(
885+
{
886+
'choices-TOTAL_FORMS': '4',
887+
'choices-INITIAL_FORMS': '0',
888+
'choices-MAX_NUM_FORMS': '4',
889+
'choices-0-choice': 'Zero',
890+
'choices-0-votes': '0',
891+
'choices-1-choice': 'One',
892+
'choices-1-votes': '1',
893+
'choices-2-choice': 'Two',
894+
'choices-2-votes': '2',
895+
'choices-3-choice': 'Three',
896+
'choices-3-votes': '3',
897+
},
898+
prefix='choices',
899+
)
900+
# This time four forms are instantiated
901+
self.assertEqual(len(formset.forms), 4)
902+
finally:
903+
formsets.DEFAULT_MAX_NUM = _old_DEFAULT_MAX_NUM
904+
847905

848906
data = {
849907
'choices-TOTAL_FORMS': '1', # the number of forms rendered

tests/regressiontests/generic_inline_admin/tests.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from django.contrib.admin.sites import AdminSite
88
from django.contrib.contenttypes.generic import (
99
generic_inlineformset_factory, GenericTabularInline)
10+
from django.forms.formsets import DEFAULT_MAX_NUM
1011
from django.forms.models import ModelForm
1112
from django.test import TestCase
1213

@@ -241,7 +242,7 @@ def test_get_formset_kwargs(self):
241242

242243
# Create a formset with default arguments
243244
formset = media_inline.get_formset(request)
244-
self.assertEqual(formset.max_num, None)
245+
self.assertEqual(formset.max_num, DEFAULT_MAX_NUM)
245246
self.assertEqual(formset.can_order, False)
246247

247248
# Create a formset with custom keyword arguments

0 commit comments

Comments
 (0)