Ticket #14445: 14445.2.2.diff
File 14445.2.2.diff, 46.5 KB (added by , 14 years ago) |
---|
-
django/contrib/auth/tests/tokens.py
diff -r 710e7a1ebcad 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 710e7a1ebcad django/contrib/auth/tokens.py
a b 1 1 from datetime import date 2 2 3 from django.conf import settings 4 from django.utils.hashcompat import sha_constructor 3 5 from django.utils.http import int_to_base36, base36_to_int 6 from django.utils.crypto import constant_time_compare, salted_hmac 4 7 5 8 class PasswordResetTokenGenerator(object): 6 9 """ … … 30 33 return False 31 34 32 35 # Check that the timestamp/uid has not been tampered with 33 if self._make_token_with_timestamp(user, ts) != token: 34 return False 36 if not constant_time_compare(self._make_token_with_timestamp(user, ts), token): 37 # Fallback to Django 1.2 method for compatibility. 38 # PendingDeprecationWarning <- here to remind us to remove this in 39 # Django 1.5 40 if not constant_time_compare(self._make_token_with_timestamp_old(user, ts), token): 41 return False 35 42 36 43 # Check the timestamp is within limit 37 44 if (self._num_days(self._today()) - ts) > settings.PASSWORD_RESET_TIMEOUT_DAYS: … … 50 57 # last_login will also change), we produce a hash that will be 51 58 # invalid as soon as it is used. 52 59 # We limit the hash to 20 chars to keep URL short 53 from django.utils.hashcompat import sha_constructor 60 key_salt = "django.contrib.auth.tokens.PasswordResetTokenGenerator" 61 value = unicode(user.id) + \ 62 user.password + user.last_login.strftime('%Y-%m-%d %H:%M:%S') + \ 63 unicode(timestamp) 64 hash = salted_hmac(key_salt, value).hexdigest()[::2] 65 return "%s-%s" % (ts_b36, hash) 66 67 def _make_token_with_timestamp_old(self, user, timestamp): 68 # The Django 1.2 method 69 ts_b36 = int_to_base36(timestamp) 54 70 hash = sha_constructor(settings.SECRET_KEY + unicode(user.id) + 55 71 user.password + user.last_login.strftime('%Y-%m-%d %H:%M:%S') + 56 72 unicode(timestamp)).hexdigest()[::2] -
django/contrib/comments/forms.py
diff -r 710e7a1ebcad django/contrib/comments/forms.py
a b 6 6 from django.conf import settings 7 7 from django.contrib.contenttypes.models import ContentType 8 8 from models import Comment 9 from django.utils.crypto import salted_hmac, constant_time_compare 9 10 from django.utils.encoding import force_unicode 10 11 from django.utils.hashcompat import sha_constructor 11 12 from django.utils.text import get_text_list … … 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_salt = "django.contrib.forms.CommentSecurityForm" 96 value = "-".join(info) 97 return salted_hmac(key_salt, value).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 710e7a1ebcad 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.crypto 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 710e7a1ebcad 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 710e7a1ebcad 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 710e7a1ebcad 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_current(self): 333 """ 334 Form should advance if the hash is present and good, as calculated using 335 current method. 336 """ 337 data = {"0-field": "test", 338 "1-field": "test2", 339 "hash_0": "7e9cea465f6a10a6fb47fcea65cb9a76350c9a5c", 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 710e7a1ebcad 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 710e7a1ebcad 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 710e7a1ebcad django/contrib/formtools/utils.py
a b 4 4 import pickle 5 5 6 6 from django.conf import settings 7 from django.forms import BooleanField 8 from django.utils.crypto import salted_hmac 7 9 from django.utils.hashcompat import md5_constructor 8 from django.forms import BooleanField 10 9 11 10 12 def security_hash(request, form, *args): 11 13 """ … … 15 17 order, pickles the result with the SECRET_KEY setting, then takes an md5 16 18 hash of that. 17 19 """ 18 20 import warnings 21 warnings.warn("security_hash is deprecated; use form_hmac instead", 22 PendingDeprecationWarning) 19 23 data = [] 20 24 for bf in form: 21 25 # Get the value from the form data. If the form allows empty or hasn't … … 37 41 38 42 return md5_constructor(pickled).hexdigest() 39 43 44 45 def form_hmac(form): 46 """ 47 Calculates a security hash for the given Form instance. 48 """ 49 data = [] 50 for bf in form: 51 # Get the value from the form data. If the form allows empty or hasn't 52 # changed then don't call clean() to avoid trigger validation errors. 53 if form.empty_permitted and not form.has_changed(): 54 value = bf.data or '' 55 else: 56 value = bf.field.clean(bf.data) or '' 57 if isinstance(value, basestring): 58 value = value.strip() 59 data.append((bf.name, value)) 60 61 pickled = pickle.dumps(data, pickle.HIGHEST_PROTOCOL) 62 key_salt = 'django.contrib.formtools' 63 return salted_hmac(key_salt, pickled).hexdigest() -
django/contrib/formtools/wizard.py
diff -r 710e7a1ebcad django/contrib/formtools/wizard.py
a b 8 8 9 9 from django import forms 10 10 from django.conf import settings 11 from django.contrib.formtools.utils import security_hash, form_hmac 11 12 from django.http import Http404 12 13 from django.shortcuts import render_to_response 13 14 from django.template.context import RequestContext 15 from django.utils.crypto import constant_time_compare 14 16 from django.utils.hashcompat import md5_constructor 15 17 from django.utils.translation import ugettext_lazy as _ 16 from django.contrib.formtools.utils import security_hash17 18 from django.utils.decorators import method_decorator 18 19 from django.views.decorators.csrf import csrf_protect 19 20 … … 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 … … 95 117 # Validate all the forms. If any of them fail validation, that 96 118 # must mean the validator relied on some other input, such as 97 119 # an external Web site. 120 121 # It is also possible that validation might fail under certain 122 # attack situations: an attacker might be able to bypass previous 123 # stages, and generate correct security hashes for all the 124 # skipped stages by virtue of: 125 # 1) having filled out an identical form which doesn't have the 126 # validation (and does something different at the end), 127 # 2) or having filled out a previous version of the same form 128 # which had some validation missing, 129 # 3) or previously having filled out the form when they had 130 # more privileges than they do now. 131 # 132 # Since the hashes only take into account values, and not other 133 # other validation the form might do, we must re-do validation 134 # now for security reasons. 98 135 for i, f in enumerate(final_form_list): 99 136 if not f.is_valid(): 100 137 return self.render_revalidation_failure(request, i, f) … … 155 192 Subclasses may want to take into account request-specific information, 156 193 such as the IP address. 157 194 """ 158 return security_hash(request,form)195 return form_hmac(form) 159 196 160 197 def determine_step(self, request, *args, **kwargs): 161 198 """ -
django/contrib/messages/storage/cookie.py
diff -r 710e7a1ebcad django/contrib/messages/storage/cookie.py
a b 1 import hmac2 3 1 from django.conf import settings 4 2 from django.contrib.messages import constants 5 3 from django.contrib.messages.storage.base import BaseStorage, Message 6 4 from django.http import CompatCookie 7 5 from django.utils import simplejson as json 8 from django.utils. hashcompat import sha_hmac6 from django.utils.crypto import salted_hmac, constant_time_compare 9 7 10 8 11 9 class MessageEncoder(json.JSONEncoder): … … 111 109 Creates an HMAC/SHA1 hash based on the value and the project setting's 112 110 SECRET_KEY, modified to make it unique for the present purpose. 113 111 """ 114 key = 'django.contrib.messages' + settings.SECRET_KEY115 return hmac.new(key, value, sha_hmac).hexdigest()112 key_salt = 'django.contrib.messages' 113 return salted_hmac(key_salt, value).hexdigest() 116 114 117 115 def _encode(self, messages, encode_empty=False): 118 116 """ … … 139 137 bits = data.split('$', 1) 140 138 if len(bits) == 2: 141 139 hash, value = bits 142 if hash == self._hash(value):140 if constant_time_compare(hash, self._hash(value)): 143 141 try: 144 142 # If we get here (and the JSON decode works), everything is 145 143 # good. In any other case, drop back and return None. -
django/contrib/sessions/backends/base.py
diff -r 710e7a1ebcad django/contrib/sessions/backends/base.py
a b 12 12 from django.conf import settings 13 13 from django.core.exceptions import SuspiciousOperation 14 14 from django.utils.hashcompat import md5_constructor 15 from django.utils.crypto import constant_time_compare, salted_hmac 15 16 16 17 # Use the system (hardware-based) random number generator if it exists. 17 18 if hasattr(random, 'SystemRandom'): … … 83 84 def delete_test_cookie(self): 84 85 del self[self.TEST_COOKIE_NAME] 85 86 87 def _hash(self, value): 88 key_salt = "django.contrib.sessions" + self.__class__.__name__ 89 return salted_hmac(key_salt, value).hexdigest() 90 86 91 def encode(self, session_dict): 87 92 "Returns the given session dictionary pickled and encoded as a string." 88 93 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)94 hash = self._hash(pickled) 95 return base64.encodestring(hash + ":" + pickled) 91 96 92 97 def decode(self, session_data): 93 98 encoded_data = base64.decodestring(session_data) 99 try: 100 # could produce ValueError if there is no ':' 101 hash, pickled = encoded_data.split(':', 1) 102 expected_hash = self._hash(pickled) 103 if not constant_time_compare(hash, expected_hash): 104 raise SuspiciousOperation("Session data corrupted") 105 else: 106 return pickle.loads(pickled) 107 except Exception: 108 # ValueError, SuspiciousOperation, unpickling exceptions 109 # Fall back to Django 1.2 method 110 # PendingDeprecationWarning <- here to remind us to 111 # remove this fallback in Django 1.5 112 try: 113 return self._decode_old(session_data) 114 except Exception: 115 # Unpickling can cause a variety of exceptions. If something happens, 116 # just return an empty dictionary (an empty session). 117 return {} 118 119 def _decode_old(self, session_data): 120 encoded_data = base64.decodestring(session_data) 94 121 pickled, tamper_check = encoded_data[:-32], encoded_data[-32:] 95 if md5_constructor(pickled + settings.SECRET_KEY).hexdigest() != tamper_check: 122 if not constant_time_compare(md5_constructor(pickled + settings.SECRET_KEY).hexdigest(), 123 tamper_check): 96 124 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 {} 125 return pickle.loads(pickled) 103 126 104 127 def update(self, dict_): 105 128 self._session.update(dict_) -
django/contrib/sessions/tests.py
diff -r 710e7a1ebcad 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 710e7a1ebcad 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.crypto 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), -
new file django/utils/crypto.py
diff -r 710e7a1ebcad django/utils/crypto.py
- + 1 """ 2 Django's standard crypto functions and utilities. 3 """ 4 import hmac 5 6 from django.conf import settings 7 from django.utils.hashcompat import sha_constructor 8 9 10 def salted_hmac(key_salt, value, secret=None): 11 """ 12 Returns the HMAC-SHA1 of 'value', using a key generated from key_salt and a 13 secret (which defaults to settings.SECRET_KEY). 14 15 A different key_salt should be passed in for every application of HMAC. 16 """ 17 if secret is None: 18 secret = settings.SECRET_KEY 19 20 # We need to generate a derived key from our base key. We can do this by 21 # passing the key_salt and our base key through a pseudo-random function and 22 # SHA1 works nicely. 23 24 key = sha_constructor(key_salt + secret).digest() 25 26 # If len(key_salt + secret) > sha_constructor().block_size, the above 27 # line is redundant and could be replaced by key = key_salt + secret, since 28 # the hmac module does the same thing for keys longer than the block size. 29 # However, we need to ensure that we *always* do this. 30 31 return hmac.new(key, msg=value, digestmod=sha_constructor) 32 33 34 def constant_time_compare(val1, val2): 35 """ 36 Returns True if the two strings are equal, False otherwise. 37 38 The time taken is independent of the number of characters that match. 39 """ 40 if len(val1) != len(val2): 41 return False 42 result = 0 43 for x, y in zip(val1, val2): 44 result |= ord(x) ^ ord(y) 45 return result == 0 -
docs/internals/deprecation.txt
diff -r 710e7a1ebcad 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 710e7a1ebcad 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 710e7a1ebcad 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())