mirror of
				https://github.com/django/django.git
				synced 2025-10-31 01:25:32 +00:00 
			
		
		
		
	[1.5.x] Added a default limit to the maximum number of forms in a formset.
This is a security fix. Disclosure and advisory coming shortly.
This commit is contained in:
		
				
					committed by
					
						 Carl Meyer
						Carl Meyer
					
				
			
			
				
	
			
			
			
						parent
						
							0e46c7f7ac
						
					
				
				
					commit
					3ef4bbf495
				
			| @@ -21,6 +21,9 @@ MAX_NUM_FORM_COUNT = 'MAX_NUM_FORMS' | ||||
| ORDERING_FIELD_NAME = 'ORDER' | ||||
| DELETION_FIELD_NAME = 'DELETE' | ||||
|  | ||||
| # default maximum number of forms in a formset, to prevent memory exhaustion | ||||
| DEFAULT_MAX_NUM = 1000 | ||||
|  | ||||
| class ManagementForm(Form): | ||||
|     """ | ||||
|     ``ManagementForm`` is used to keep track of how many form instances | ||||
| @@ -97,11 +100,10 @@ class BaseFormSet(object): | ||||
|             total_forms = initial_forms + self.extra | ||||
|             # Allow all existing related objects/inlines to be displayed, | ||||
|             # but don't allow extra beyond max_num. | ||||
|             if self.max_num is not None: | ||||
|                 if initial_forms > self.max_num >= 0: | ||||
|                     total_forms = initial_forms | ||||
|                 elif total_forms > self.max_num >= 0: | ||||
|                     total_forms = self.max_num | ||||
|             if initial_forms > self.max_num >= 0: | ||||
|                 total_forms = initial_forms | ||||
|             elif total_forms > self.max_num >= 0: | ||||
|                 total_forms = self.max_num | ||||
|         return total_forms | ||||
|  | ||||
|     def initial_form_count(self): | ||||
| @@ -111,14 +113,14 @@ class BaseFormSet(object): | ||||
|         else: | ||||
|             # Use the length of the inital data if it's there, 0 otherwise. | ||||
|             initial_forms = self.initial and len(self.initial) or 0 | ||||
|             if self.max_num is not None and (initial_forms > self.max_num >= 0): | ||||
|             if initial_forms > self.max_num >= 0: | ||||
|                 initial_forms = self.max_num | ||||
|         return initial_forms | ||||
|  | ||||
|     def _construct_forms(self): | ||||
|         # instantiate all the forms and put them in self.forms | ||||
|         self.forms = [] | ||||
|         for i in xrange(self.total_form_count()): | ||||
|         for i in xrange(min(self.total_form_count(), self.absolute_max)): | ||||
|             self.forms.append(self._construct_form(i)) | ||||
|  | ||||
|     def _construct_form(self, i, **kwargs): | ||||
| @@ -368,9 +370,14 @@ class BaseFormSet(object): | ||||
| def formset_factory(form, formset=BaseFormSet, extra=1, can_order=False, | ||||
|                     can_delete=False, max_num=None): | ||||
|     """Return a FormSet for the given form class.""" | ||||
|     if max_num is None: | ||||
|         max_num = DEFAULT_MAX_NUM | ||||
|     # hard limit on forms instantiated, to prevent memory-exhaustion attacks | ||||
|     # limit defaults to DEFAULT_MAX_NUM, but developer can increase it via max_num | ||||
|     absolute_max = max(DEFAULT_MAX_NUM, max_num) | ||||
|     attrs = {'form': form, 'extra': extra, | ||||
|              'can_order': can_order, 'can_delete': can_delete, | ||||
|              'max_num': max_num} | ||||
|              'max_num': max_num, 'absolute_max': absolute_max} | ||||
|     return type(form.__name__ + str('FormSet'), (formset,), attrs) | ||||
|  | ||||
| def all_valid(formsets): | ||||
|   | ||||
| @@ -98,8 +98,8 @@ If the value of ``max_num`` is greater than the number of existing | ||||
| objects, up to ``extra`` additional blank forms will be added to the formset, | ||||
| so long as the total number of forms does not exceed ``max_num``. | ||||
|  | ||||
| A ``max_num`` value of ``None`` (the default) puts no limit on the number of | ||||
| forms displayed. | ||||
| A ``max_num`` value of ``None`` (the default) puts a high limit on the number | ||||
| of forms displayed (1000). In practice this is equivalent to no limit. | ||||
|  | ||||
| Formset validation | ||||
| ------------------ | ||||
|   | ||||
| @@ -727,8 +727,8 @@ so long as the total number of forms does not exceed ``max_num``:: | ||||
|     <tr><th><label for="id_form-2-name">Name:</label></th><td><input id="id_form-2-name" type="text" name="form-2-name" value="Walt Whitman" maxlength="100" /><input type="hidden" name="form-2-id" value="2" id="id_form-2-id" /></td></tr> | ||||
|     <tr><th><label for="id_form-3-name">Name:</label></th><td><input id="id_form-3-name" type="text" name="form-3-name" maxlength="100" /><input type="hidden" name="form-3-id" id="id_form-3-id" /></td></tr> | ||||
|  | ||||
| A ``max_num`` value of ``None`` (the default) puts no limit on the number of | ||||
| forms displayed. | ||||
| A ``max_num`` value of ``None`` (the default) puts a high limit on the number | ||||
| of forms displayed (1000). In practice this is equivalent to no limit. | ||||
|  | ||||
| Using a model formset in a view | ||||
| ------------------------------- | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
| from __future__ import unicode_literals | ||||
|  | ||||
| from django.forms import (CharField, DateField, FileField, Form, IntegerField, | ||||
|     ValidationError) | ||||
|     ValidationError, formsets) | ||||
| from django.forms.formsets import BaseFormSet, formset_factory | ||||
| from django.forms.util import ErrorList | ||||
| from django.test import TestCase | ||||
| @@ -51,7 +51,7 @@ class FormsFormsetTestCase(TestCase): | ||||
|         # for adding data. By default, it displays 1 blank form. It can display more, | ||||
|         # but we'll look at how to do so later. | ||||
|         formset = ChoiceFormSet(auto_id=False, prefix='choices') | ||||
|         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" /> | ||||
|         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" /> | ||||
| <tr><th>Choice:</th><td><input type="text" name="choices-0-choice" /></td></tr> | ||||
| <tr><th>Votes:</th><td><input type="text" name="choices-0-votes" /></td></tr>""") | ||||
|  | ||||
| @@ -654,8 +654,8 @@ class FormsFormsetTestCase(TestCase): | ||||
|         # Limiting the maximum number of forms ######################################## | ||||
|         # Base case for max_num. | ||||
|  | ||||
|         # When not passed, max_num will take its default value of None, i.e. unlimited | ||||
|         # number of forms, only controlled by the value of the extra parameter. | ||||
|         # When not passed, max_num will take a high default value, leaving the | ||||
|         # number of forms only controlled by the value of the extra parameter. | ||||
|  | ||||
|         LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=3) | ||||
|         formset = LimitedFavoriteDrinkFormSet() | ||||
| @@ -702,8 +702,8 @@ class FormsFormsetTestCase(TestCase): | ||||
|     def test_max_num_with_initial_data(self): | ||||
|         # max_num with initial data | ||||
|  | ||||
|         # When not passed, max_num will take its default value of None, i.e. unlimited | ||||
|         # number of forms, only controlled by the values of the initial and extra | ||||
|         # When not passed, max_num will take a high default value, leaving the | ||||
|         # number of forms only controlled by the value of the initial and extra | ||||
|         # parameters. | ||||
|  | ||||
|         initial = [ | ||||
| @@ -878,6 +878,64 @@ class FormsFormsetTestCase(TestCase): | ||||
|         self.assertTrue(formset.is_valid()) | ||||
|         self.assertTrue(all([form.is_valid_called for form in formset.forms])) | ||||
|  | ||||
|     def test_hard_limit_on_instantiated_forms(self): | ||||
|         """A formset has a hard limit on the number of forms instantiated.""" | ||||
|         # reduce the default limit of 1000 temporarily for testing | ||||
|         _old_DEFAULT_MAX_NUM = formsets.DEFAULT_MAX_NUM | ||||
|         try: | ||||
|             formsets.DEFAULT_MAX_NUM = 3 | ||||
|             ChoiceFormSet = formset_factory(Choice) | ||||
|             # someone fiddles with the mgmt form data... | ||||
|             formset = ChoiceFormSet( | ||||
|                 { | ||||
|                     'choices-TOTAL_FORMS': '4', | ||||
|                     'choices-INITIAL_FORMS': '0', | ||||
|                     'choices-MAX_NUM_FORMS': '4', | ||||
|                     'choices-0-choice': 'Zero', | ||||
|                     'choices-0-votes': '0', | ||||
|                     'choices-1-choice': 'One', | ||||
|                     'choices-1-votes': '1', | ||||
|                     'choices-2-choice': 'Two', | ||||
|                     'choices-2-votes': '2', | ||||
|                     'choices-3-choice': 'Three', | ||||
|                     'choices-3-votes': '3', | ||||
|                     }, | ||||
|                 prefix='choices', | ||||
|                 ) | ||||
|             # But we still only instantiate 3 forms | ||||
|             self.assertEqual(len(formset.forms), 3) | ||||
|         finally: | ||||
|             formsets.DEFAULT_MAX_NUM = _old_DEFAULT_MAX_NUM | ||||
|  | ||||
|     def test_increase_hard_limit(self): | ||||
|         """Can increase the built-in forms limit via a higher max_num.""" | ||||
|         # reduce the default limit of 1000 temporarily for testing | ||||
|         _old_DEFAULT_MAX_NUM = formsets.DEFAULT_MAX_NUM | ||||
|         try: | ||||
|             formsets.DEFAULT_MAX_NUM = 3 | ||||
|             # for this form, we want a limit of 4 | ||||
|             ChoiceFormSet = formset_factory(Choice, max_num=4) | ||||
|             formset = ChoiceFormSet( | ||||
|                 { | ||||
|                     'choices-TOTAL_FORMS': '4', | ||||
|                     'choices-INITIAL_FORMS': '0', | ||||
|                     'choices-MAX_NUM_FORMS': '4', | ||||
|                     'choices-0-choice': 'Zero', | ||||
|                     'choices-0-votes': '0', | ||||
|                     'choices-1-choice': 'One', | ||||
|                     'choices-1-votes': '1', | ||||
|                     'choices-2-choice': 'Two', | ||||
|                     'choices-2-votes': '2', | ||||
|                     'choices-3-choice': 'Three', | ||||
|                     'choices-3-votes': '3', | ||||
|                     }, | ||||
|                 prefix='choices', | ||||
|                 ) | ||||
|             # This time four forms are instantiated | ||||
|             self.assertEqual(len(formset.forms), 4) | ||||
|         finally: | ||||
|             formsets.DEFAULT_MAX_NUM = _old_DEFAULT_MAX_NUM | ||||
|  | ||||
|  | ||||
| data = { | ||||
|     'choices-TOTAL_FORMS': '1', # the number of forms rendered | ||||
|   | ||||
| @@ -6,6 +6,7 @@ from django.contrib import admin | ||||
| from django.contrib.admin.sites import AdminSite | ||||
| from django.contrib.contenttypes.generic import ( | ||||
|     generic_inlineformset_factory, GenericTabularInline) | ||||
| from django.forms.formsets import DEFAULT_MAX_NUM | ||||
| from django.forms.models import ModelForm | ||||
| from django.test import TestCase | ||||
| from django.test.utils import override_settings | ||||
| @@ -244,7 +245,7 @@ class GenericInlineModelAdminTest(TestCase): | ||||
|  | ||||
|         # Create a formset with default arguments | ||||
|         formset = media_inline.get_formset(request) | ||||
|         self.assertEqual(formset.max_num, None) | ||||
|         self.assertEqual(formset.max_num, DEFAULT_MAX_NUM) | ||||
|         self.assertEqual(formset.can_order, False) | ||||
|  | ||||
|         # Create a formset with custom keyword arguments | ||||
|   | ||||
		Reference in New Issue
	
	Block a user