[1.2.X] Fixed #14498 - Forms passed to FormWizard.process_step are not guaranteed to have cleaned_data

Thanks to stas for the report.

Backport of [14290] from trunk.

git-svn-id: http://code.djangoproject.com/svn/django/branches/releases/1.2.X@14291 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Luke Plant 2010-10-19 21:45:51 +00:00
parent 6a369d2ff3
commit a7a91c2d40
2 changed files with 67 additions and 21 deletions

View File

@ -1,5 +1,6 @@
import unittest import unittest
from django import forms from django import forms
from django.conf import settings
from django.contrib.formtools import preview, wizard, utils from django.contrib.formtools import preview, wizard, utils
from django import http from django import http
from django.test import TestCase from django.test import TestCase
@ -145,6 +146,9 @@ class WizardPageOneForm(forms.Form):
class WizardPageTwoForm(forms.Form): class WizardPageTwoForm(forms.Form):
field = forms.CharField() field = forms.CharField()
class WizardPageThreeForm(forms.Form):
field = forms.CharField()
class WizardClass(wizard.FormWizard): class WizardClass(wizard.FormWizard):
def render_template(self, *args, **kw): def render_template(self, *args, **kw):
return http.HttpResponse("") return http.HttpResponse("")
@ -161,6 +165,15 @@ class DummyRequest(http.HttpRequest):
self._dont_enforce_csrf_checks = True self._dont_enforce_csrf_checks = True
class WizardTests(TestCase): class WizardTests(TestCase):
def setUp(self):
# Use a known SECRET_KEY to make security_hash tests deterministic
self.old_SECRET_KEY = settings.SECRET_KEY
settings.SECRET_KEY = "123"
def tearDown(self):
settings.SECRET_KEY = self.old_SECRET_KEY
def test_step_starts_at_zero(self): def test_step_starts_at_zero(self):
""" """
step should be zero for the first form step should be zero for the first form
@ -179,3 +192,25 @@ class WizardTests(TestCase):
response = wizard(request) response = wizard(request)
self.assertEquals(1, wizard.step) self.assertEquals(1, wizard.step)
def test_14498(self):
"""
Regression test for ticket #14498.
"""
that = self
reached = [False]
class WizardWithProcessStep(WizardClass):
def process_step(self, request, form, step):
reached[0] = True
that.assertTrue(hasattr(form, 'cleaned_data'))
wizard = WizardWithProcessStep([WizardPageOneForm,
WizardPageTwoForm,
WizardPageThreeForm])
data = {"0-field": "test",
"1-field": "test2",
"hash_0": "2fdbefd4c0cad51509478fbacddf8b13",
"wizard_step": "1"}
wizard(DummyRequest(POST=data))
self.assertTrue(reached[0])

View File

@ -68,39 +68,50 @@ class FormWizard(object):
if current_step >= self.num_steps(): if current_step >= self.num_steps():
raise Http404('Step %s does not exist' % current_step) raise Http404('Step %s does not exist' % current_step)
# For each previous step, verify the hash and process.
# TODO: Move "hash_%d" to a method to make it configurable.
for i in range(current_step):
form = self.get_form(i, request.POST)
if request.POST.get("hash_%d" % i, '') != self.security_hash(request, form):
return self.render_hash_failure(request, i)
self.process_step(request, form, i)
# Process the current step. If it's valid, go to the next step or call # Process the current step. If it's valid, go to the next step or call
# done(), depending on whether any steps remain. # done(), depending on whether any steps remain.
if request.method == 'POST': if request.method == 'POST':
form = self.get_form(current_step, request.POST) form = self.get_form(current_step, request.POST)
else: else:
form = self.get_form(current_step) form = self.get_form(current_step)
if form.is_valid(): if form.is_valid():
# Validate all the forms. If any of them fail validation, that
# must mean the validator relied on some other input, such as
# an external Web site.
# It is also possible that validation might fail under certain
# attack situations: an attacker might be able to bypass previous
# stages, and generate correct security hashes for all the
# skipped stages by virtue of:
# 1) having filled out an identical form which doesn't have the
# validation (and does something different at the end),
# 2) or having filled out a previous version of the same form
# which had some validation missing,
# 3) or previously having filled out the form when they had
# more privileges than they do now.
#
# Since the hashes only take into account values, and not other
# other validation the form might do, we must re-do validation
# now for security reasons.
current_form_list = [self.get_form(i, request.POST) for i in range(current_step)]
for i, f in enumerate(current_form_list):
if request.POST.get("hash_%d" % i, '') != self.security_hash(request, f):
return self.render_hash_failure(request, i)
if not f.is_valid():
return self.render_revalidation_failure(request, i, f)
else:
self.process_step(request, f, i)
# Now progress to processing this step:
self.process_step(request, form, current_step) self.process_step(request, form, current_step)
next_step = current_step + 1 next_step = current_step + 1
# If this was the last step, validate all of the forms one more
# time, as a sanity check, and call done().
num = self.num_steps()
if next_step == num:
final_form_list = [self.get_form(i, request.POST) for i in range(num)]
# Validate all the forms. If any of them fail validation, that if current_step == self.num_steps():
# must mean the validator relied on some other input, such as
# an external Web site.
for i, f in enumerate(final_form_list):
if not f.is_valid():
return self.render_revalidation_failure(request, i, f)
return self.done(request, final_form_list) return self.done(request, final_form_list)
# Otherwise, move along to the next step.
else: else:
form = self.get_form(next_step) form = self.get_form(next_step)
self.step = current_step = next_step self.step = current_step = next_step