Ticket #14445: 14445.2.diff

File 14445.2.diff, 45.2 KB (added by Luke Plant, 14 years ago)

Updated to ensure we do SHA1 on the key

  • django/contrib/auth/tests/tokens.py

    diff -r 710e7a1ebcad django/contrib/auth/tests/tokens.py
    a b  
    5050
    5151        p2 = Mocked(date.today() + timedelta(settings.PASSWORD_RESET_TIMEOUT_DAYS + 1))
    5252        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  
    11from datetime import date
     2
    23from django.conf import settings
     4from django.utils.hashcompat import sha_constructor
    35from django.utils.http import int_to_base36, base36_to_int
     6from django.utils.crypto import constant_time_compare, salted_hmac
    47
    58class PasswordResetTokenGenerator(object):
    69    """
     
    3033            return False
    3134
    3235        # 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
    3542
    3643        # Check the timestamp is within limit
    3744        if (self._num_days(self._today()) - ts) > settings.PASSWORD_RESET_TIMEOUT_DAYS:
     
    5057        # last_login will also change), we produce a hash that will be
    5158        # invalid as soon as it is used.
    5259        # 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)
    5470        hash = sha_constructor(settings.SECRET_KEY + unicode(user.id) +
    5571                               user.password + user.last_login.strftime('%Y-%m-%d %H:%M:%S') +
    5672                               unicode(timestamp)).hexdigest()[::2]
  • django/contrib/comments/forms.py

    diff -r 710e7a1ebcad django/contrib/comments/forms.py
    a b  
    66from django.conf import settings
    77from django.contrib.contenttypes.models import ContentType
    88from models import Comment
     9from django.utils.crypto import salted_hmac, constant_time_compare
    910from django.utils.encoding import force_unicode
    1011from django.utils.hashcompat import sha_constructor
    1112from django.utils.text import get_text_list
     
    4647        }
    4748        expected_hash = self.generate_security_hash(**security_hash_dict)
    4849        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.")
    5157        return actual_hash
    5258
    5359    def clean_timestamp(self):
     
    8288        return self.generate_security_hash(**initial_security_dict)
    8389
    8490    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):
    85100        """Generate a (SHA1) security hash from the provided info."""
     101        # Django 1.2 compatibility
    86102        info = (content_type, object_pk, timestamp, settings.SECRET_KEY)
    87103        return sha_constructor("".join(info)).hexdigest()
    88104
  • django/contrib/formtools/preview.py

    diff -r 710e7a1ebcad django/contrib/formtools/preview.py
    a b  
    99from django.shortcuts import render_to_response
    1010from django.template.context import RequestContext
    1111from django.utils.hashcompat import md5_constructor
     12from django.utils.crypto import constant_time_compare
    1213from django.contrib.formtools.utils import security_hash
    1314
    1415AUTO_ID = 'formtools_%s' # Each form here uses this as its auto_id parameter.
     
    6768        else:
    6869            return render_to_response(self.form_template, context, context_instance=RequestContext(request))
    6970
     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
    7092    def post_post(self, request):
    7193        "Validates the POST data. If valid, calls done(). Else, redisplays form."
    7294        f = self.form(request.POST, auto_id=AUTO_ID)
    7395        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):
    7598                return self.failed_hash(request) # Security hash failed.
    7699            return self.done(request, f.cleaned_data)
    77100        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 forms
    2 from django import http
    3 from django.contrib.formtools import preview, wizard, utils
    4 from django.test import TestCase
    5 from django.utils import unittest
    6 
    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 tests
    24         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 retrieve
    40         the form (mostly testing the setup ROOT_URLCONF
    41         process). Verify that an additional  hidden input field
    42         is created to manage the stage.
    43 
    44         """
    45         response = self.client.get('/test1/')
    46         stage = self.input % 1
    47         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 preview
    54         is returned.  If we do get a form back check that the hidden
    55         value is correctly managing the state of the form.
    56 
    57         """
    58         # Pass strings for form submittal and add stage variable to
    59         # 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 % 2
    64         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 3
    71         to see if our forms done() method is called. Check first
    72         without the security hash, verify failure, retry with security
    73         hash and verify sucess.
    74 
    75         """
    76         # Pass strings for form submittal and add stage variable to
    77         # 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 preview
    92         form's hash would be computed with no value for ``bool1``. However, when
    93         the preview form is rendered, the unchecked hidden BooleanField would be
    94         rendered with the string value 'False'. So when the preview form is
    95         resubmitted, the hash would be computed with the value 'False' for
    96         ``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 ignore
    110         leading/trailing whitespace so as to be friendly to broken browsers that
    111         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 with
    122         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 tests
    140 #
    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 = True
    162 
    163 class WizardTests(TestCase):
    164     def test_step_starts_at_zero(self):
    165         """
    166         step should be zero for the first form
    167         """
    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 page
    176         """
    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
    - +  
     1import os
     2
     3from django import forms
     4from django import http
     5from django.conf import settings
     6from django.contrib.formtools import preview, wizard, utils
     7from django.test import TestCase
     8from django.utils import unittest
     9
     10success_string = "Done was called!"
     11
     12
     13class TestFormPreview(preview.FormPreview):
     14
     15    def done(self, request, cleaned_data):
     16        return http.HttpResponse(success_string)
     17
     18
     19class TestForm(forms.Form):
     20    field1 = forms.CharField()
     21    field1_ = forms.CharField()
     22    bool1 = forms.BooleanField(required=False)
     23
     24
     25class UserSecuredFormPreview(TestFormPreview):
     26    """
     27    FormPreview with a custum security_hash method
     28    """
     29    def security_hash(self, request, form):
     30        return "123"
     31
     32
     33class 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
     152class 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
     178class 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
     207class HashTestForm(forms.Form):
     208    name = forms.CharField()
     209    bio = forms.CharField()
     210
     211
     212class HashTestBlankForm(forms.Form):
     213    name = forms.CharField(required=False)
     214    bio = forms.CharField(required=False)
     215
     216#
     217# FormWizard tests
     218#
     219
     220
     221class WizardPageOneForm(forms.Form):
     222    field = forms.CharField()
     223
     224
     225class WizardPageTwoForm(forms.Form):
     226    field = forms.CharField()
     227
     228
     229class WizardPageThreeForm(forms.Form):
     230    field = forms.CharField()
     231
     232
     233class 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
     242class UserSecuredWizardClass(WizardClass):
     243    """
     244    Wizard with a custum security_hash method
     245    """
     246    def security_hash(self, request, form):
     247        return "123"
     248
     249
     250class 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
     260class 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"""
     2This is a URLconf to be loaded by tests.py. Add any URLs needed for tests only.
     3"""
     4
     5from django.conf.urls.defaults import *
     6from django.contrib.formtools.tests import *
     7
     8urlpatterns = 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  
    44    import pickle
    55
    66from django.conf import settings
     7from django.forms import BooleanField
     8from django.utils.crypto import salted_hmac
    79from django.utils.hashcompat import md5_constructor
    8 from django.forms import BooleanField
     10
    911
    1012def security_hash(request, form, *args):
    1113    """
     
    1517    order, pickles the result with the SECRET_KEY setting, then takes an md5
    1618    hash of that.
    1719    """
    18 
     20    import warnings
     21    warnings.warn("security_hash is deprecated; use form_hmac instead",
     22                  PendingDeprecationWarning)
    1923    data = []
    2024    for bf in form:
    2125        # Get the value from the form data. If the form allows empty or hasn't
     
    3741
    3842    return md5_constructor(pickled).hexdigest()
    3943
     44
     45def form_hmac(form):
     46    """
     47    Calculates a security hash for the given Form instance.  'key_extra' should
     48    be a string that is unique to a particular form wizard.
     49    """
     50    data = []
     51    for bf in form:
     52        # Get the value from the form data. If the form allows empty or hasn't
     53        # changed then don't call clean() to avoid trigger validation errors.
     54        if form.empty_permitted and not form.has_changed():
     55            value = bf.data or ''
     56        else:
     57            value = bf.field.clean(bf.data) or ''
     58        if isinstance(value, basestring):
     59            value = value.strip()
     60        data.append((bf.name, value))
     61
     62    pickled = pickle.dumps(data, pickle.HIGHEST_PROTOCOL)
     63    key_salt = 'django.contrib.formtools'
     64    return salted_hmac(key_salt, pickled).hexdigest()
  • django/contrib/formtools/wizard.py

    diff -r 710e7a1ebcad django/contrib/formtools/wizard.py
    a b  
    88
    99from django import forms
    1010from django.conf import settings
     11from django.contrib.formtools.utils import security_hash, form_hmac
    1112from django.http import Http404
    1213from django.shortcuts import render_to_response
    1314from django.template.context import RequestContext
     15from django.utils.crypto import constant_time_compare
    1416from django.utils.hashcompat import md5_constructor
    1517from django.utils.translation import ugettext_lazy as _
    16 from django.contrib.formtools.utils import security_hash
    1718from django.utils.decorators import method_decorator
    1819from django.views.decorators.csrf import csrf_protect
    1920
     
    5354        # hook methods might alter self.form_list.
    5455        return len(self.form_list)
    5556
     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
    5678    @method_decorator(csrf_protect)
    5779    def __call__(self, request, *args, **kwargs):
    5880        """
     
    7294        # TODO: Move "hash_%d" to a method to make it configurable.
    7395        for i in range(current_step):
    7496            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):
    7698                return self.render_hash_failure(request, i)
    7799            self.process_step(request, form, i)
    78100
     
    155177        Subclasses may want to take into account request-specific information,
    156178        such as the IP address.
    157179        """
    158         return security_hash(request, form)
     180        return form_hmac(form)
    159181
    160182    def determine_step(self, request, *args, **kwargs):
    161183        """
  • django/contrib/messages/storage/cookie.py

    diff -r 710e7a1ebcad django/contrib/messages/storage/cookie.py
    a b  
    1 import hmac
    2 
    31from django.conf import settings
    42from django.contrib.messages import constants
    53from django.contrib.messages.storage.base import BaseStorage, Message
    64from django.http import CompatCookie
    75from django.utils import simplejson as json
    8 from django.utils.hashcompat import sha_hmac
     6from django.utils.crypto import salted_hmac, constant_time_compare
    97
    108
    119class MessageEncoder(json.JSONEncoder):
     
    111109        Creates an HMAC/SHA1 hash based on the value and the project setting's
    112110        SECRET_KEY, modified to make it unique for the present purpose.
    113111        """
    114         key = 'django.contrib.messages' + settings.SECRET_KEY
    115         return hmac.new(key, value, sha_hmac).hexdigest()
     112        key_salt = 'django.contrib.messages'
     113        return salted_hmac(key_salt, value).hexdigest()
    116114
    117115    def _encode(self, messages, encode_empty=False):
    118116        """
     
    139137        bits = data.split('$', 1)
    140138        if len(bits) == 2:
    141139            hash, value = bits
    142             if hash == self._hash(value):
     140            if constant_time_compare(hash, self._hash(value)):
    143141                try:
    144142                    # If we get here (and the JSON decode works), everything is
    145143                    # 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  
    1212from django.conf import settings
    1313from django.core.exceptions import SuspiciousOperation
    1414from django.utils.hashcompat import md5_constructor
     15from django.utils.crypto import constant_time_compare, salted_hmac
    1516
    1617# Use the system (hardware-based) random number generator if it exists.
    1718if hasattr(random, 'SystemRandom'):
     
    8384    def delete_test_cookie(self):
    8485        del self[self.TEST_COOKIE_NAME]
    8586
     87    def _hash(self, value):
     88        key_salt = "django.contrib.sessions" + self.__class__.__name__
     89        return salted_hmac(key_salt, value).hexdigest()
     90
    8691    def encode(self, session_dict):
    8792        "Returns the given session dictionary pickled and encoded as a string."
    8893        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)
    9196
    9297    def decode(self, session_data):
    9398        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)
    94121        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):
    96124            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)
    103126
    104127    def update(self, dict_):
    105128        self._session.update(dict_)
  • django/contrib/sessions/tests.py

    diff -r 710e7a1ebcad django/contrib/sessions/tests.py
    a b  
     1import base64
    12from datetime import datetime, timedelta
     3import pickle
    24import shutil
    35import tempfile
    46
     
    1214from django.core.exceptions import ImproperlyConfigured
    1315from django.test import TestCase
    1416from django.utils import unittest
     17from django.utils.hashcompat import md5_constructor
    1518
    1619
    1720class SessionTestsMixin(object):
     
    237240        finally:
    238241            settings.SESSION_EXPIRE_AT_BROWSER_CLOSE = original_expire_at_browser_close
    239242
     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
    240261
    241262class DatabaseSessionTests(SessionTestsMixin, TestCase):
    242263
  • django/middleware/csrf.py

    diff -r 710e7a1ebcad django/middleware/csrf.py
    a b  
    1515from django.utils.hashcompat import md5_constructor
    1616from django.utils.log import getLogger
    1717from django.utils.safestring import mark_safe
     18from django.utils.crypto import constant_time_compare
    1819
    1920_POST_FORM_RE = \
    2021    re.compile(r'(<form\W[^>]*\bmethod\s*=\s*(\'|"|)POST(\'|"|)\b[^>]*>)', re.IGNORECASE)
     
    216217                csrf_token = request.META["CSRF_COOKIE"]
    217218
    218219            # 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):
    221222                if cookie_is_new:
    222223                    # probably a problem setting the CSRF cookie
    223224                    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"""
     2Django's standard crypto functions and utilities.
     3"""
     4import hmac
     5
     6from django.conf import settings
     7from django.utils.hashcompat import sha_constructor
     8
     9
     10def 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
     34def 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  
    114114          :class:`~django.test.simple.DjangoTestRunner` will be removed in
    115115          favor of using the unittest-native class.
    116116
     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
    117121    * 2.0
    118122        * ``django.views.defaults.shortcut()``. This function has been moved
    119123          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  
    240240    Calculates the security hash for the given request object and
    241241    :class:`~django.forms.Form` instance.
    242242
    243     By default, this uses an MD5 hash of the form data and your
     243    By default, this generates a SHA1 HMAC using your form data and your
    244244    :setting:`SECRET_KEY` setting. It's rare that somebody would need to
    245245    override this.
    246246
  • tests/regressiontests/comment_tests/tests/comment_form_tests.py

    diff -r 710e7a1ebcad tests/regressiontests/comment_tests/tests/comment_form_tests.py
    a b  
    22from django.conf import settings
    33from django.contrib.comments.models import Comment
    44from django.contrib.comments.forms import CommentForm
     5from django.utils.hashcompat import sha_constructor
    56from regressiontests.comment_tests.models import Article
    67from regressiontests.comment_tests.tests import CommentTestCase
    78
     
    4344    def testObjectPKTampering(self):
    4445        self.tamperWithForm(object_pk="3")
    4546
     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
    4664    def testSecurityErrors(self):
    4765        f = self.tamperWithForm(honeypot="I am a robot")
    4866        self.assert_("honeypot" in f.security_errors())
Back to Top