Ticket #14445: 14445.diff
File 14445.diff, 44.0 KB (added by , 14 years ago) |
---|
-
django/contrib/auth/tests/tokens.py
diff -r a7ef72553337 django/contrib/auth/tests/tokens.py
a b 50 50 51 51 p2 = Mocked(date.today() + timedelta(settings.PASSWORD_RESET_TIMEOUT_DAYS + 1)) 52 52 self.assertFalse(p2.check_token(user, tk1)) 53 54 def test_django12_hash(self): 55 """ 56 Ensure we can use the hashes generated by Django 1.2 57 """ 58 # Hard code in the Django 1.2 algorithm (not the result, as it is time 59 # dependent) 60 def _make_token(user): 61 from django.utils.hashcompat import sha_constructor 62 from django.utils.http import int_to_base36 63 64 timestamp = (date.today() - date(2001,1,1)).days 65 ts_b36 = int_to_base36(timestamp) 66 hash = sha_constructor(settings.SECRET_KEY + unicode(user.id) + 67 user.password + user.last_login.strftime('%Y-%m-%d %H:%M:%S') + 68 unicode(timestamp)).hexdigest()[::2] 69 return "%s-%s" % (ts_b36, hash) 70 71 user = User.objects.create_user('tokentestuser', 'test2@example.com', 'testpw') 72 p0 = PasswordResetTokenGenerator() 73 tk1 = _make_token(user) 74 self.assertTrue(p0.check_token(user, tk1)) -
django/contrib/auth/tokens.py
diff -r a7ef72553337 django/contrib/auth/tokens.py
a b 1 1 from datetime import date 2 import hmac 3 2 4 from django.conf import settings 5 from django.utils.hashcompat import sha_constructor, sha_hmac 3 6 from django.utils.http import int_to_base36, base36_to_int 7 from django.utils.text import constant_time_compare 4 8 5 9 class PasswordResetTokenGenerator(object): 6 10 """ … … 30 34 return False 31 35 32 36 # Check that the timestamp/uid has not been tampered with 33 if self._make_token_with_timestamp(user, ts) != token: 34 return False 37 if not constant_time_compare(self._make_token_with_timestamp(user, ts), token): 38 # Fallback to Django 1.2 method for compatibility. 39 # PendingDeprecationWarning <- here to remind us to remove this in 40 # Django 1.5 41 if not constant_time_compare(self._make_token_with_timestamp_old(user, ts), token): 42 return False 35 43 36 44 # Check the timestamp is within limit 37 45 if (self._num_days(self._today()) - ts) > settings.PASSWORD_RESET_TIMEOUT_DAYS: … … 50 58 # last_login will also change), we produce a hash that will be 51 59 # invalid as soon as it is used. 52 60 # We limit the hash to 20 chars to keep URL short 53 from django.utils.hashcompat import sha_constructor 61 key = "django.contrib.auth.tokens.PasswordResetTokenGenerator" + settings.SECRET_KEY 62 value = unicode(user.id) + \ 63 user.password + user.last_login.strftime('%Y-%m-%d %H:%M:%S') + \ 64 unicode(timestamp) 65 hash = hmac.new(key, value, sha_hmac).hexdigest()[::2] 66 return "%s-%s" % (ts_b36, hash) 67 68 def _make_token_with_timestamp_old(self, user, timestamp): 69 # The Django 1.2 method 70 ts_b36 = int_to_base36(timestamp) 54 71 hash = sha_constructor(settings.SECRET_KEY + unicode(user.id) + 55 72 user.password + user.last_login.strftime('%Y-%m-%d %H:%M:%S') + 56 73 unicode(timestamp)).hexdigest()[::2] -
django/contrib/comments/forms.py
diff -r a7ef72553337 django/contrib/comments/forms.py
a b 1 import hmac 1 2 import time 2 3 import datetime 3 4 … … 7 8 from django.contrib.contenttypes.models import ContentType 8 9 from models import Comment 9 10 from django.utils.encoding import force_unicode 10 from django.utils.hashcompat import sha_ constructor11 from django.utils.text import get_text_list 11 from django.utils.hashcompat import sha_hmac, sha_constructor 12 from django.utils.text import get_text_list, constant_time_compare 12 13 from django.utils.translation import ungettext, ugettext_lazy as _ 13 14 14 15 COMMENT_MAX_LENGTH = getattr(settings,'COMMENT_MAX_LENGTH', 3000) … … 46 47 } 47 48 expected_hash = self.generate_security_hash(**security_hash_dict) 48 49 actual_hash = self.cleaned_data["security_hash"] 49 if expected_hash != actual_hash: 50 raise forms.ValidationError("Security hash check failed.") 50 if not constant_time_compare(expected_hash, actual_hash): 51 # Fallback to Django 1.2 method for compatibility 52 # PendingDeprecationWarning <- here to remind us to remove this 53 # fallback in Django 1.5 54 expected_hash_old = self._generate_security_hash_old(**security_hash_dict) 55 if not constant_time_compare(expected_hash_old, actual_hash): 56 raise forms.ValidationError("Security hash check failed.") 51 57 return actual_hash 52 58 53 59 def clean_timestamp(self): … … 82 88 return self.generate_security_hash(**initial_security_dict) 83 89 84 90 def generate_security_hash(self, content_type, object_pk, timestamp): 91 """ 92 Generate a HMAC security hash from the provided info. 93 """ 94 info = (content_type, object_pk, timestamp) 95 key = "django.contrib.forms.CommentSecurityForm" + settings.SECRET_KEY 96 value = "-".join(info) 97 return hmac.new(key, value, sha_hmac).hexdigest() 98 99 def _generate_security_hash_old(self, content_type, object_pk, timestamp): 85 100 """Generate a (SHA1) security hash from the provided info.""" 101 # Django 1.2 compatibility 86 102 info = (content_type, object_pk, timestamp, settings.SECRET_KEY) 87 103 return sha_constructor("".join(info)).hexdigest() 88 104 -
django/contrib/formtools/preview.py
diff -r a7ef72553337 django/contrib/formtools/preview.py
a b 9 9 from django.shortcuts import render_to_response 10 10 from django.template.context import RequestContext 11 11 from django.utils.hashcompat import md5_constructor 12 from django.utils.text import constant_time_compare 12 13 from django.contrib.formtools.utils import security_hash 13 14 14 15 AUTO_ID = 'formtools_%s' # Each form here uses this as its auto_id parameter. … … 67 68 else: 68 69 return render_to_response(self.form_template, context, context_instance=RequestContext(request)) 69 70 71 def _check_security_hash(self, token, request, form): 72 expected = self.security_hash(request, form) 73 if constant_time_compare(token, expected): 74 return True 75 else: 76 # Fall back to Django 1.2 method, for compatibility with forms that 77 # are in the middle of being used when the upgrade occurs. However, 78 # we don't want to do this fallback if a subclass has provided their 79 # own security_hash method - because they might have implemented a 80 # more secure method, and this would punch a hole in that. 81 82 # PendingDeprecationWarning <- left here to remind us that this 83 # compatibility fallback should be removed in Django 1.5 84 FormPreview_expected = FormPreview.security_hash(self, request, form) 85 if expected == FormPreview_expected: 86 # They didn't override security_hash, do the fallback: 87 old_expected = security_hash(request, form) 88 return constant_time_compare(token, old_expected) 89 else: 90 return False 91 70 92 def post_post(self, request): 71 93 "Validates the POST data. If valid, calls done(). Else, redisplays form." 72 94 f = self.form(request.POST, auto_id=AUTO_ID) 73 95 if f.is_valid(): 74 if self.security_hash(request, f) != request.POST.get(self.unused_name('hash')): 96 if not self._check_security_hash(request.POST.get(self.unused_name('hash'), ''), 97 request, f): 75 98 return self.failed_hash(request) # Security hash failed. 76 99 return self.done(request, f.cleaned_data) 77 100 else: -
deleted file django/contrib/formtools/test_urls.py
diff -r a7ef72553337 django/contrib/formtools/test_urls.py
+ - 1 """2 This is a URLconf to be loaded by tests.py. Add any URLs needed for tests only.3 """4 5 from django.conf.urls.defaults import *6 from django.contrib.formtools.tests import *7 8 urlpatterns = patterns('',9 (r'^test1/', TestFormPreview(TestForm)),10 ) -
deleted file django/contrib/formtools/tests.py
diff -r a7ef72553337 django/contrib/formtools/tests.py
+ - 1 from django import forms2 from django import http3 from django.contrib.formtools import preview, wizard, utils4 from django.test import TestCase5 from django.utils import unittest6 7 success_string = "Done was called!"8 9 class TestFormPreview(preview.FormPreview):10 11 def done(self, request, cleaned_data):12 return http.HttpResponse(success_string)13 14 class TestForm(forms.Form):15 field1 = forms.CharField()16 field1_ = forms.CharField()17 bool1 = forms.BooleanField(required=False)18 19 class PreviewTests(TestCase):20 urls = 'django.contrib.formtools.test_urls'21 22 def setUp(self):23 # Create a FormPreview instance to share between tests24 self.preview = preview.FormPreview(TestForm)25 input_template = '<input type="hidden" name="%s" value="%s" />'26 self.input = input_template % (self.preview.unused_name('stage'), "%d")27 self.test_data = {'field1':u'foo', 'field1_':u'asdf'}28 29 def test_unused_name(self):30 """31 Verifies name mangling to get uniue field name.32 """33 self.assertEqual(self.preview.unused_name('field1'), 'field1__')34 35 def test_form_get(self):36 """37 Test contrib.formtools.preview form retrieval.38 39 Use the client library to see if we can sucessfully retrieve40 the form (mostly testing the setup ROOT_URLCONF41 process). Verify that an additional hidden input field42 is created to manage the stage.43 44 """45 response = self.client.get('/test1/')46 stage = self.input % 147 self.assertContains(response, stage, 1)48 49 def test_form_preview(self):50 """51 Test contrib.formtools.preview form preview rendering.52 53 Use the client library to POST to the form to see if a preview54 is returned. If we do get a form back check that the hidden55 value is correctly managing the state of the form.56 57 """58 # Pass strings for form submittal and add stage variable to59 # show we previously saw first stage of the form.60 self.test_data.update({'stage': 1})61 response = self.client.post('/test1/', self.test_data)62 # Check to confirm stage is set to 2 in output form.63 stage = self.input % 264 self.assertContains(response, stage, 1)65 66 def test_form_submit(self):67 """68 Test contrib.formtools.preview form submittal.69 70 Use the client library to POST to the form with stage set to 371 to see if our forms done() method is called. Check first72 without the security hash, verify failure, retry with security73 hash and verify sucess.74 75 """76 # Pass strings for form submittal and add stage variable to77 # show we previously saw first stage of the form.78 self.test_data.update({'stage':2})79 response = self.client.post('/test1/', self.test_data)80 self.failIfEqual(response.content, success_string)81 hash = self.preview.security_hash(None, TestForm(self.test_data))82 self.test_data.update({'hash': hash})83 response = self.client.post('/test1/', self.test_data)84 self.assertEqual(response.content, success_string)85 86 def test_bool_submit(self):87 """88 Test contrib.formtools.preview form submittal when form contains:89 BooleanField(required=False)90 91 Ticket: #6209 - When an unchecked BooleanField is previewed, the preview92 form's hash would be computed with no value for ``bool1``. However, when93 the preview form is rendered, the unchecked hidden BooleanField would be94 rendered with the string value 'False'. So when the preview form is95 resubmitted, the hash would be computed with the value 'False' for96 ``bool1``. We need to make sure the hashes are the same in both cases.97 98 """99 self.test_data.update({'stage':2})100 hash = self.preview.security_hash(None, TestForm(self.test_data))101 self.test_data.update({'hash':hash, 'bool1':u'False'})102 response = self.client.post('/test1/', self.test_data)103 self.assertEqual(response.content, success_string)104 105 class SecurityHashTests(unittest.TestCase):106 107 def test_textfield_hash(self):108 """109 Regression test for #10034: the hash generation function should ignore110 leading/trailing whitespace so as to be friendly to broken browsers that111 submit it (usually in textareas).112 """113 f1 = HashTestForm({'name': 'joe', 'bio': 'Nothing notable.'})114 f2 = HashTestForm({'name': ' joe', 'bio': 'Nothing notable. '})115 hash1 = utils.security_hash(None, f1)116 hash2 = utils.security_hash(None, f2)117 self.assertEqual(hash1, hash2)118 119 def test_empty_permitted(self):120 """121 Regression test for #10643: the security hash should allow forms with122 empty_permitted = True, or forms where data has not changed.123 """124 f1 = HashTestBlankForm({})125 f2 = HashTestForm({}, empty_permitted=True)126 hash1 = utils.security_hash(None, f1)127 hash2 = utils.security_hash(None, f2)128 self.assertEqual(hash1, hash2)129 130 class HashTestForm(forms.Form):131 name = forms.CharField()132 bio = forms.CharField()133 134 class HashTestBlankForm(forms.Form):135 name = forms.CharField(required=False)136 bio = forms.CharField(required=False)137 138 #139 # FormWizard tests140 #141 142 class WizardPageOneForm(forms.Form):143 field = forms.CharField()144 145 class WizardPageTwoForm(forms.Form):146 field = forms.CharField()147 148 class WizardClass(wizard.FormWizard):149 def render_template(self, *args, **kw):150 return http.HttpResponse("")151 152 def done(self, request, cleaned_data):153 return http.HttpResponse(success_string)154 155 class DummyRequest(http.HttpRequest):156 def __init__(self, POST=None):157 super(DummyRequest, self).__init__()158 self.method = POST and "POST" or "GET"159 if POST is not None:160 self.POST.update(POST)161 self._dont_enforce_csrf_checks = True162 163 class WizardTests(TestCase):164 def test_step_starts_at_zero(self):165 """166 step should be zero for the first form167 """168 wizard = WizardClass([WizardPageOneForm, WizardPageTwoForm])169 request = DummyRequest()170 wizard(request)171 self.assertEquals(0, wizard.step)172 173 def test_step_increments(self):174 """175 step should be incremented when we go to the next page176 """177 wizard = WizardClass([WizardPageOneForm, WizardPageTwoForm])178 request = DummyRequest(POST={"0-field":"test", "wizard_step":"0"})179 response = wizard(request)180 self.assertEquals(1, wizard.step)181 -
new file django/contrib/formtools/tests/__init__.py
diff -r a7ef72553337 django/contrib/formtools/tests/__init__.py
- + 1 import os 2 3 from django import forms 4 from django import http 5 from django.conf import settings 6 from django.contrib.formtools import preview, wizard, utils 7 from django.test import TestCase 8 from django.utils import unittest 9 10 success_string = "Done was called!" 11 12 13 class TestFormPreview(preview.FormPreview): 14 15 def done(self, request, cleaned_data): 16 return http.HttpResponse(success_string) 17 18 19 class TestForm(forms.Form): 20 field1 = forms.CharField() 21 field1_ = forms.CharField() 22 bool1 = forms.BooleanField(required=False) 23 24 25 class UserSecuredFormPreview(TestFormPreview): 26 """ 27 FormPreview with a custum security_hash method 28 """ 29 def security_hash(self, request, form): 30 return "123" 31 32 33 class PreviewTests(TestCase): 34 urls = 'django.contrib.formtools.tests.urls' 35 36 def setUp(self): 37 # Create a FormPreview instance to share between tests 38 self.preview = preview.FormPreview(TestForm) 39 input_template = '<input type="hidden" name="%s" value="%s" />' 40 self.input = input_template % (self.preview.unused_name('stage'), "%d") 41 self.test_data = {'field1':u'foo', 'field1_':u'asdf'} 42 43 def test_unused_name(self): 44 """ 45 Verifies name mangling to get uniue field name. 46 """ 47 self.assertEqual(self.preview.unused_name('field1'), 'field1__') 48 49 def test_form_get(self): 50 """ 51 Test contrib.formtools.preview form retrieval. 52 53 Use the client library to see if we can sucessfully retrieve 54 the form (mostly testing the setup ROOT_URLCONF 55 process). Verify that an additional hidden input field 56 is created to manage the stage. 57 58 """ 59 response = self.client.get('/test1/') 60 stage = self.input % 1 61 self.assertContains(response, stage, 1) 62 63 def test_form_preview(self): 64 """ 65 Test contrib.formtools.preview form preview rendering. 66 67 Use the client library to POST to the form to see if a preview 68 is returned. If we do get a form back check that the hidden 69 value is correctly managing the state of the form. 70 71 """ 72 # Pass strings for form submittal and add stage variable to 73 # show we previously saw first stage of the form. 74 self.test_data.update({'stage': 1}) 75 response = self.client.post('/test1/', self.test_data) 76 # Check to confirm stage is set to 2 in output form. 77 stage = self.input % 2 78 self.assertContains(response, stage, 1) 79 80 def test_form_submit(self): 81 """ 82 Test contrib.formtools.preview form submittal. 83 84 Use the client library to POST to the form with stage set to 3 85 to see if our forms done() method is called. Check first 86 without the security hash, verify failure, retry with security 87 hash and verify sucess. 88 89 """ 90 # Pass strings for form submittal and add stage variable to 91 # show we previously saw first stage of the form. 92 self.test_data.update({'stage':2}) 93 response = self.client.post('/test1/', self.test_data) 94 self.failIfEqual(response.content, success_string) 95 hash = self.preview.security_hash(None, TestForm(self.test_data)) 96 self.test_data.update({'hash': hash}) 97 response = self.client.post('/test1/', self.test_data) 98 self.assertEqual(response.content, success_string) 99 100 def test_bool_submit(self): 101 """ 102 Test contrib.formtools.preview form submittal when form contains: 103 BooleanField(required=False) 104 105 Ticket: #6209 - When an unchecked BooleanField is previewed, the preview 106 form's hash would be computed with no value for ``bool1``. However, when 107 the preview form is rendered, the unchecked hidden BooleanField would be 108 rendered with the string value 'False'. So when the preview form is 109 resubmitted, the hash would be computed with the value 'False' for 110 ``bool1``. We need to make sure the hashes are the same in both cases. 111 112 """ 113 self.test_data.update({'stage':2}) 114 hash = self.preview.security_hash(None, TestForm(self.test_data)) 115 self.test_data.update({'hash':hash, 'bool1':u'False'}) 116 response = self.client.post('/test1/', self.test_data) 117 self.assertEqual(response.content, success_string) 118 119 def test_form_submit_django12_hash(self): 120 """ 121 Test contrib.formtools.preview form submittal, using the hash function 122 used in Django 1.2 123 """ 124 # Pass strings for form submittal and add stage variable to 125 # show we previously saw first stage of the form. 126 self.test_data.update({'stage':2}) 127 response = self.client.post('/test1/', self.test_data) 128 self.failIfEqual(response.content, success_string) 129 hash = utils.security_hash(None, TestForm(self.test_data)) 130 self.test_data.update({'hash': hash}) 131 response = self.client.post('/test1/', self.test_data) 132 self.assertEqual(response.content, success_string) 133 134 135 def test_form_submit_django12_hash_custom_hash(self): 136 """ 137 Test contrib.formtools.preview form submittal, using the hash function 138 used in Django 1.2 and a custom security_hash method. 139 """ 140 # Pass strings for form submittal and add stage variable to 141 # show we previously saw first stage of the form. 142 self.test_data.update({'stage':2}) 143 response = self.client.post('/test2/', self.test_data) 144 self.assertEqual(response.status_code, 200) 145 self.failIfEqual(response.content, success_string) 146 hash = utils.security_hash(None, TestForm(self.test_data)) 147 self.test_data.update({'hash': hash}) 148 response = self.client.post('/test2/', self.test_data) 149 self.failIfEqual(response.content, success_string) 150 151 152 class SecurityHashTests(unittest.TestCase): 153 154 def test_textfield_hash(self): 155 """ 156 Regression test for #10034: the hash generation function should ignore 157 leading/trailing whitespace so as to be friendly to broken browsers that 158 submit it (usually in textareas). 159 """ 160 f1 = HashTestForm({'name': 'joe', 'bio': 'Nothing notable.'}) 161 f2 = HashTestForm({'name': ' joe', 'bio': 'Nothing notable. '}) 162 hash1 = utils.security_hash(None, f1) 163 hash2 = utils.security_hash(None, f2) 164 self.assertEqual(hash1, hash2) 165 166 def test_empty_permitted(self): 167 """ 168 Regression test for #10643: the security hash should allow forms with 169 empty_permitted = True, or forms where data has not changed. 170 """ 171 f1 = HashTestBlankForm({}) 172 f2 = HashTestForm({}, empty_permitted=True) 173 hash1 = utils.security_hash(None, f1) 174 hash2 = utils.security_hash(None, f2) 175 self.assertEqual(hash1, hash2) 176 177 178 class FormHmacTests(unittest.TestCase): 179 """ 180 Same as SecurityHashTests, but with form_hmac 181 """ 182 183 def test_textfield_hash(self): 184 """ 185 Regression test for #10034: the hash generation function should ignore 186 leading/trailing whitespace so as to be friendly to broken browsers that 187 submit it (usually in textareas). 188 """ 189 f1 = HashTestForm({'name': 'joe', 'bio': 'Nothing notable.'}) 190 f2 = HashTestForm({'name': ' joe', 'bio': 'Nothing notable. '}) 191 hash1 = utils.form_hmac(f1) 192 hash2 = utils.form_hmac(f2) 193 self.assertEqual(hash1, hash2) 194 195 def test_empty_permitted(self): 196 """ 197 Regression test for #10643: the security hash should allow forms with 198 empty_permitted = True, or forms where data has not changed. 199 """ 200 f1 = HashTestBlankForm({}) 201 f2 = HashTestForm({}, empty_permitted=True) 202 hash1 = utils.form_hmac(f1) 203 hash2 = utils.form_hmac(f2) 204 self.assertEqual(hash1, hash2) 205 206 207 class HashTestForm(forms.Form): 208 name = forms.CharField() 209 bio = forms.CharField() 210 211 212 class HashTestBlankForm(forms.Form): 213 name = forms.CharField(required=False) 214 bio = forms.CharField(required=False) 215 216 # 217 # FormWizard tests 218 # 219 220 221 class WizardPageOneForm(forms.Form): 222 field = forms.CharField() 223 224 225 class WizardPageTwoForm(forms.Form): 226 field = forms.CharField() 227 228 229 class WizardPageThreeForm(forms.Form): 230 field = forms.CharField() 231 232 233 class WizardClass(wizard.FormWizard): 234 235 def get_template(self, step): 236 return 'formwizard/wizard.html' 237 238 def done(self, request, cleaned_data): 239 return http.HttpResponse(success_string) 240 241 242 class UserSecuredWizardClass(WizardClass): 243 """ 244 Wizard with a custum security_hash method 245 """ 246 def security_hash(self, request, form): 247 return "123" 248 249 250 class DummyRequest(http.HttpRequest): 251 252 def __init__(self, POST=None): 253 super(DummyRequest, self).__init__() 254 self.method = POST and "POST" or "GET" 255 if POST is not None: 256 self.POST.update(POST) 257 self._dont_enforce_csrf_checks = True 258 259 260 class WizardTests(TestCase): 261 urls = 'django.contrib.formtools.tests.urls' 262 263 def setUp(self): 264 self.old_TEMPLATE_DIRS = settings.TEMPLATE_DIRS 265 settings.TEMPLATE_DIRS = ( 266 os.path.join( 267 os.path.dirname(__file__), 268 'templates' 269 ), 270 ) 271 # Use a known SECRET_KEY to make security_hash tests deterministic 272 self.old_SECRET_KEY = settings.SECRET_KEY 273 settings.SECRET_KEY = "123" 274 275 def tearDown(self): 276 settings.TEMPLATE_DIRS = self.old_TEMPLATE_DIRS 277 settings.SECRET_KEY = self.old_SECRET_KEY 278 279 def test_step_starts_at_zero(self): 280 """ 281 step should be zero for the first form 282 """ 283 response = self.client.get('/wizard/') 284 self.assertEquals(0, response.context['step0']) 285 286 def test_step_increments(self): 287 """ 288 step should be incremented when we go to the next page 289 """ 290 response = self.client.post('/wizard/', {"0-field":"test", "wizard_step":"0"}) 291 self.assertEquals(1, response.context['step0']) 292 293 def test_bad_hash(self): 294 """ 295 Form should not advance if the hash is missing or bad 296 """ 297 response = self.client.post('/wizard/', 298 {"0-field":"test", 299 "1-field":"test2", 300 "wizard_step": "1"}) 301 self.assertEquals(0, response.context['step0']) 302 303 def test_good_hash_django12(self): 304 """ 305 Form should advance if the hash is present and good, as calculated using 306 django 1.2 method. 307 """ 308 # We are hard-coding a hash value here, but that is OK, since we want to 309 # ensure that we don't accidentally change the algorithm. 310 data = {"0-field": "test", 311 "1-field": "test2", 312 "hash_0": "2fdbefd4c0cad51509478fbacddf8b13", 313 "wizard_step": "1"} 314 response = self.client.post('/wizard/', data) 315 self.assertEquals(2, response.context['step0']) 316 317 def test_good_hash_django12_subclass(self): 318 """ 319 The Django 1.2 method of calulating hashes should *not* be used as a 320 fallback if the FormWizard subclass has provided their own method 321 of calculating a hash. 322 """ 323 # We are hard-coding a hash value here, but that is OK, since we want to 324 # ensure that we don't accidentally change the algorithm. 325 data = {"0-field": "test", 326 "1-field": "test2", 327 "hash_0": "2fdbefd4c0cad51509478fbacddf8b13", 328 "wizard_step": "1"} 329 response = self.client.post('/wizard2/', data) 330 self.assertEquals(0, response.context['step0']) 331 332 def test_good_hash_django13(self): 333 """ 334 Form should advance if the hash is present and good, as calculated using 335 django 1.3 method. 336 """ 337 data = {"0-field": "test", 338 "1-field": "test2", 339 "hash_0": "d662b87b56e906966801a96e49aed87559a8538e", 340 "wizard_step": "1"} 341 response = self.client.post('/wizard/', data) 342 self.assertEquals(2, response.context['step0']) -
new file django/contrib/formtools/tests/templates/formwizard/wizard.html
diff -r a7ef72553337 django/contrib/formtools/tests/templates/formwizard/wizard.html
- + 1 <p>Step {{ step }} of {{ step_count }}</p> 2 <form action="." method="post">{% csrf_token %} 3 <table> 4 {{ form }} 5 </table> 6 <input type="hidden" name="{{ step_field }}" value="{{ step0 }}" /> 7 {{ previous_fields|safe }} 8 <input type="submit"> 9 </form> -
new file django/contrib/formtools/tests/urls.py
diff -r a7ef72553337 django/contrib/formtools/tests/urls.py
- + 1 """ 2 This is a URLconf to be loaded by tests.py. Add any URLs needed for tests only. 3 """ 4 5 from django.conf.urls.defaults import * 6 from django.contrib.formtools.tests import * 7 8 urlpatterns = patterns('', 9 (r'^test1/', TestFormPreview(TestForm)), 10 (r'^test2/', UserSecuredFormPreview(TestForm)), 11 (r'^wizard/$', WizardClass([WizardPageOneForm, 12 WizardPageTwoForm, 13 WizardPageThreeForm])), 14 (r'^wizard2/$', UserSecuredWizardClass([WizardPageOneForm, 15 WizardPageTwoForm, 16 WizardPageThreeForm])) 17 ) -
django/contrib/formtools/utils.py
diff -r a7ef72553337 django/contrib/formtools/utils.py
a b 3 3 except ImportError: 4 4 import pickle 5 5 6 import hmac 7 6 8 from django.conf import settings 7 from django.utils.hashcompat import md5_constructor 9 from django.utils.hashcompat import md5_constructor, sha_hmac 8 10 from django.forms import BooleanField 9 11 12 10 13 def security_hash(request, form, *args): 11 14 """ 12 15 Calculates a security hash for the given Form instance. … … 15 18 order, pickles the result with the SECRET_KEY setting, then takes an md5 16 19 hash of that. 17 20 """ 18 21 import warnings 22 warnings.warn("security_hash is deprecated; use form_hmac instead", 23 PendingDeprecationWarning) 19 24 data = [] 20 25 for bf in form: 21 26 # Get the value from the form data. If the form allows empty or hasn't … … 37 42 38 43 return md5_constructor(pickled).hexdigest() 39 44 45 46 def form_hmac(form): 47 """ 48 Calculates a security hash for the given Form instance. 'key_extra' should 49 be a string that is unique to a particular form wizard. 50 """ 51 data = [] 52 for bf in form: 53 # Get the value from the form data. If the form allows empty or hasn't 54 # changed then don't call clean() to avoid trigger validation errors. 55 if form.empty_permitted and not form.has_changed(): 56 value = bf.data or '' 57 else: 58 value = bf.field.clean(bf.data) or '' 59 if isinstance(value, basestring): 60 value = value.strip() 61 data.append((bf.name, value)) 62 63 pickled = pickle.dumps(data, pickle.HIGHEST_PROTOCOL) 64 key = 'django.contrib.formtools' + settings.SECRET_KEY 65 return hmac.new(key, pickled, sha_hmac).hexdigest() -
django/contrib/formtools/wizard.py
diff -r a7ef72553337 django/contrib/formtools/wizard.py
a b 13 13 from django.template.context import RequestContext 14 14 from django.utils.hashcompat import md5_constructor 15 15 from django.utils.translation import ugettext_lazy as _ 16 from django.contrib.formtools.utils import security_hash 16 from django.contrib.formtools.utils import security_hash, form_hmac 17 17 from django.utils.decorators import method_decorator 18 from django.utils.text import constant_time_compare 18 19 from django.views.decorators.csrf import csrf_protect 19 20 20 21 … … 53 54 # hook methods might alter self.form_list. 54 55 return len(self.form_list) 55 56 57 def _check_security_hash(self, token, request, form): 58 expected = self.security_hash(request, form) 59 if constant_time_compare(token, expected): 60 return True 61 else: 62 # Fall back to Django 1.2 method, for compatibility with forms that 63 # are in the middle of being used when the upgrade occurs. However, 64 # we don't want to do this fallback if a subclass has provided their 65 # own security_hash method - because they might have implemented a 66 # more secure method, and this would punch a hole in that. 67 68 # PendingDeprecationWarning <- left here to remind us that this 69 # compatibility fallback should be removed in Django 1.5 70 FormWizard_expected = FormWizard.security_hash(self, request, form) 71 if expected == FormWizard_expected: 72 # They didn't override security_hash, do the fallback: 73 old_expected = security_hash(request, form) 74 return constant_time_compare(token, old_expected) 75 else: 76 return False 77 56 78 @method_decorator(csrf_protect) 57 79 def __call__(self, request, *args, **kwargs): 58 80 """ … … 72 94 # TODO: Move "hash_%d" to a method to make it configurable. 73 95 for i in range(current_step): 74 96 form = self.get_form(i, request.POST) 75 if request.POST.get("hash_%d" % i, '') != self.security_hash(request, form):97 if not self._check_security_hash(request.POST.get("hash_%d" % i, ''), request, form): 76 98 return self.render_hash_failure(request, i) 77 99 self.process_step(request, form, i) 78 100 … … 155 177 Subclasses may want to take into account request-specific information, 156 178 such as the IP address. 157 179 """ 158 return security_hash(request,form)180 return form_hmac(form) 159 181 160 182 def determine_step(self, request, *args, **kwargs): 161 183 """ -
django/contrib/messages/storage/cookie.py
diff -r a7ef72553337 django/contrib/messages/storage/cookie.py
a b 6 6 from django.http import CompatCookie 7 7 from django.utils import simplejson as json 8 8 from django.utils.hashcompat import sha_hmac 9 from django.utils.text import constant_time_compare 9 10 10 11 11 12 class MessageEncoder(json.JSONEncoder): … … 139 140 bits = data.split('$', 1) 140 141 if len(bits) == 2: 141 142 hash, value = bits 142 if hash == self._hash(value):143 if constant_time_compare(hash, self._hash(value)): 143 144 try: 144 145 # If we get here (and the JSON decode works), everything is 145 146 # good. In any other case, drop back and return None. -
django/contrib/sessions/backends/base.py
diff -r a7ef72553337 django/contrib/sessions/backends/base.py
a b 1 1 import base64 2 import hmac 2 3 import os 3 4 import random 4 5 import sys … … 11 12 12 13 from django.conf import settings 13 14 from django.core.exceptions import SuspiciousOperation 14 from django.utils.hashcompat import md5_constructor 15 from django.utils.hashcompat import md5_constructor, sha_hmac 16 from django.utils.text import constant_time_compare 15 17 16 18 # Use the system (hardware-based) random number generator if it exists. 17 19 if hasattr(random, 'SystemRandom'): … … 83 85 def delete_test_cookie(self): 84 86 del self[self.TEST_COOKIE_NAME] 85 87 88 def _hash(self, value): 89 key = "django.contrib.sessions" + self.__class__.__name__ + settings.SECRET_KEY 90 return hmac.new(key, value, sha_hmac).hexdigest() 91 86 92 def encode(self, session_dict): 87 93 "Returns the given session dictionary pickled and encoded as a string." 88 94 pickled = pickle.dumps(session_dict, pickle.HIGHEST_PROTOCOL) 89 pickled_md5 = md5_constructor(pickled + settings.SECRET_KEY).hexdigest()90 return base64.encodestring( pickled + pickled_md5)95 hash = self._hash(pickled) 96 return base64.encodestring(hash + ":" + pickled) 91 97 92 98 def decode(self, session_data): 93 99 encoded_data = base64.decodestring(session_data) 100 try: 101 # could produce ValueError if there is no ':' 102 hash, pickled = encoded_data.split(':', 1) 103 expected_hash = self._hash(pickled) 104 if not constant_time_compare(hash, expected_hash): 105 raise SuspiciousOperation("Session data corrupted") 106 else: 107 return pickle.loads(pickled) 108 except Exception: 109 # ValueError, SuspiciousOperation, unpickling exceptions 110 # Fall back to Django 1.2 method 111 # PendingDeprecationWarning <- here to remind us to 112 # remove this fallback in Django 1.5 113 try: 114 return self._decode_old(session_data) 115 except Exception: 116 # Unpickling can cause a variety of exceptions. If something happens, 117 # just return an empty dictionary (an empty session). 118 return {} 119 120 def _decode_old(self, session_data): 121 encoded_data = base64.decodestring(session_data) 94 122 pickled, tamper_check = encoded_data[:-32], encoded_data[-32:] 95 if md5_constructor(pickled + settings.SECRET_KEY).hexdigest() != tamper_check: 123 if not constant_time_compare(md5_constructor(pickled + settings.SECRET_KEY).hexdigest(), 124 tamper_check): 96 125 raise SuspiciousOperation("User tampered with session cookie.") 97 try: 98 return pickle.loads(pickled) 99 # Unpickling can cause a variety of exceptions. If something happens, 100 # just return an empty dictionary (an empty session). 101 except: 102 return {} 126 return pickle.loads(pickled) 103 127 104 128 def update(self, dict_): 105 129 self._session.update(dict_) -
django/contrib/sessions/tests.py
diff -r a7ef72553337 django/contrib/sessions/tests.py
a b 1 import base64 1 2 from datetime import datetime, timedelta 3 import pickle 2 4 import shutil 3 5 import tempfile 4 6 … … 12 14 from django.core.exceptions import ImproperlyConfigured 13 15 from django.test import TestCase 14 16 from django.utils import unittest 17 from django.utils.hashcompat import md5_constructor 15 18 16 19 17 20 class SessionTestsMixin(object): … … 237 240 finally: 238 241 settings.SESSION_EXPIRE_AT_BROWSER_CLOSE = original_expire_at_browser_close 239 242 243 def test_decode(self): 244 # Ensure we can decode what we encode 245 data = {'a test key': 'a test value'} 246 encoded = self.session.encode(data) 247 self.assertEqual(self.session.decode(encoded), data) 248 249 def test_decode_django12(self): 250 # Ensure we can decode values encoded using Django 1.2 251 # Hard code the Django 1.2 method here: 252 def encode(session_dict): 253 pickled = pickle.dumps(session_dict, pickle.HIGHEST_PROTOCOL) 254 pickled_md5 = md5_constructor(pickled + settings.SECRET_KEY).hexdigest() 255 return base64.encodestring(pickled + pickled_md5) 256 257 data = {'a test key': 'a test value'} 258 encoded = encode(data) 259 self.assertEqual(self.session.decode(encoded), data) 260 240 261 241 262 class DatabaseSessionTests(SessionTestsMixin, TestCase): 242 263 -
django/middleware/csrf.py
diff -r a7ef72553337 django/middleware/csrf.py
a b 15 15 from django.utils.hashcompat import md5_constructor 16 16 from django.utils.log import getLogger 17 17 from django.utils.safestring import mark_safe 18 from django.utils.text import constant_time_compare 18 19 19 20 _POST_FORM_RE = \ 20 21 re.compile(r'(<form\W[^>]*\bmethod\s*=\s*(\'|"|)POST(\'|"|)\b[^>]*>)', re.IGNORECASE) … … 216 217 csrf_token = request.META["CSRF_COOKIE"] 217 218 218 219 # check incoming token 219 request_csrf_token = request.POST.get('csrfmiddlewaretoken', None)220 if request_csrf_token != csrf_token:220 request_csrf_token = request.POST.get('csrfmiddlewaretoken', '') 221 if not constant_time_compare(request_csrf_token, csrf_token): 221 222 if cookie_is_new: 222 223 # probably a problem setting the CSRF cookie 223 224 logger.warning('Forbidden (%s): %s' % (REASON_NO_CSRF_COOKIE, request.path), -
django/utils/text.py
diff -r a7ef72553337 django/utils/text.py
a b 280 280 quote = s[0] 281 281 return s[1:-1].replace(r'\%s' % quote, quote).replace(r'\\', '\\') 282 282 unescape_string_literal = allow_lazy(unescape_string_literal) 283 284 def constant_time_compare(val1, val2): 285 """ 286 Returns True if the two strings are equal, False otherwise. 287 288 The time taken is independent of the number of characters that match. 289 """ 290 if len(val1) != len(val2): 291 return False 292 result = 0 293 for x, y in zip(val1, val2): 294 result |= ord(x) ^ ord(y) 295 return result == 0 -
docs/internals/deprecation.txt
diff -r a7ef72553337 docs/internals/deprecation.txt
a b 114 114 :class:`~django.test.simple.DjangoTestRunner` will be removed in 115 115 favor of using the unittest-native class. 116 116 117 * The undocumented function 118 :func:`django.contrib.formtools.utils.security_hash` 119 is deprecated, in favour of :func:`django.contrib.formtools.utils.form_hmac` 120 117 121 * 2.0 118 122 * ``django.views.defaults.shortcut()``. This function has been moved 119 123 to ``django.contrib.contenttypes.views.shortcut()`` as part of the -
docs/ref/contrib/formtools/form-wizard.txt
diff -r a7ef72553337 docs/ref/contrib/formtools/form-wizard.txt
a b 240 240 Calculates the security hash for the given request object and 241 241 :class:`~django.forms.Form` instance. 242 242 243 By default, this uses an MD5 hash of theform data and your243 By default, this generates a SHA1 HMAC using your form data and your 244 244 :setting:`SECRET_KEY` setting. It's rare that somebody would need to 245 245 override this. 246 246 -
tests/regressiontests/comment_tests/tests/comment_form_tests.py
diff -r a7ef72553337 tests/regressiontests/comment_tests/tests/comment_form_tests.py
a b 2 2 from django.conf import settings 3 3 from django.contrib.comments.models import Comment 4 4 from django.contrib.comments.forms import CommentForm 5 from django.utils.hashcompat import sha_constructor 5 6 from regressiontests.comment_tests.models import Article 6 7 from regressiontests.comment_tests.tests import CommentTestCase 7 8 … … 43 44 def testObjectPKTampering(self): 44 45 self.tamperWithForm(object_pk="3") 45 46 47 def testDjango12Hash(self): 48 # Ensure we can use the hashes generated by Django 1.2 49 a = Article.objects.get(pk=1) 50 d = self.getValidData(a) 51 52 content_type = d['content_type'] 53 object_pk = d['object_pk'] 54 timestamp = d['timestamp'] 55 56 # The Django 1.2 method hard-coded here: 57 info = (content_type, object_pk, timestamp, settings.SECRET_KEY) 58 security_hash = sha_constructor("".join(info)).hexdigest() 59 60 d['security_hash'] = security_hash 61 f = CommentForm(a, data=d) 62 self.assertTrue(f.is_valid(), f.errors) 63 46 64 def testSecurityErrors(self): 47 65 f = self.tamperWithForm(honeypot="I am a robot") 48 66 self.assert_("honeypot" in f.security_errors())