Ticket #14445: 14445.diff

File 14445.diff, 44.0 KB (added by Luke Plant, 14 years ago)
  • django/contrib/auth/tests/tokens.py

    diff -r a7ef72553337 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 a7ef72553337 django/contrib/auth/tokens.py
    a b  
    11from datetime import date
     2import hmac
     3
    24from django.conf import settings
     5from django.utils.hashcompat import sha_constructor, sha_hmac
    36from django.utils.http import int_to_base36, base36_to_int
     7from django.utils.text import constant_time_compare
    48
    59class PasswordResetTokenGenerator(object):
    610    """
     
    3034            return False
    3135
    3236        # Check that the timestamp/uid has not been tampered with
    33         if self._make_token_with_timestamp(user, ts) != token:
    34             return False
     37        if not constant_time_compare(self._make_token_with_timestamp(user, ts), token):
     38            # Fallback to Django 1.2 method for compatibility.
     39            # PendingDeprecationWarning <- here to remind us to remove this in
     40            # Django 1.5
     41            if not constant_time_compare(self._make_token_with_timestamp_old(user, ts), token):
     42                return False
    3543
    3644        # Check the timestamp is within limit
    3745        if (self._num_days(self._today()) - ts) > settings.PASSWORD_RESET_TIMEOUT_DAYS:
     
    5058        # last_login will also change), we produce a hash that will be
    5159        # invalid as soon as it is used.
    5260        # We limit the hash to 20 chars to keep URL short
    53         from django.utils.hashcompat import sha_constructor
     61        key = "django.contrib.auth.tokens.PasswordResetTokenGenerator" + settings.SECRET_KEY
     62        value = unicode(user.id) + \
     63            user.password + user.last_login.strftime('%Y-%m-%d %H:%M:%S') + \
     64            unicode(timestamp)
     65        hash = hmac.new(key, value, sha_hmac).hexdigest()[::2]
     66        return "%s-%s" % (ts_b36, hash)
     67
     68    def _make_token_with_timestamp_old(self, user, timestamp):
     69        # The Django 1.2 method
     70        ts_b36 = int_to_base36(timestamp)
    5471        hash = sha_constructor(settings.SECRET_KEY + unicode(user.id) +
    5572                               user.password + user.last_login.strftime('%Y-%m-%d %H:%M:%S') +
    5673                               unicode(timestamp)).hexdigest()[::2]
  • django/contrib/comments/forms.py

    diff -r a7ef72553337 django/contrib/comments/forms.py
    a b  
     1import hmac
    12import time
    23import datetime
    34
     
    78from django.contrib.contenttypes.models import ContentType
    89from models import Comment
    910from django.utils.encoding import force_unicode
    10 from django.utils.hashcompat import sha_constructor
    11 from django.utils.text import get_text_list
     11from django.utils.hashcompat import sha_hmac, sha_constructor
     12from django.utils.text import get_text_list, constant_time_compare
    1213from django.utils.translation import ungettext, ugettext_lazy as _
    1314
    1415COMMENT_MAX_LENGTH = getattr(settings,'COMMENT_MAX_LENGTH', 3000)
     
    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 = "django.contrib.forms.CommentSecurityForm" + settings.SECRET_KEY
     96        value = "-".join(info)
     97        return hmac.new(key, value, sha_hmac).hexdigest()
     98
     99    def _generate_security_hash_old(self, content_type, object_pk, timestamp):
    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 a7ef72553337 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.text 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 a7ef72553337 django/contrib/formtools/test_urls.py
    + -  
    1 """
    2 This is a URLconf to be loaded by tests.py. Add any URLs needed for tests only.
    3 """
    4 
    5 from django.conf.urls.defaults import *
    6 from django.contrib.formtools.tests import *
    7 
    8 urlpatterns = patterns('',
    9                        (r'^test1/', TestFormPreview(TestForm)),
    10                       )
  • deleted file django/contrib/formtools/tests.py

    diff -r a7ef72553337 django/contrib/formtools/tests.py
    + -  
    1 from django import 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 a7ef72553337 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_django13(self):
     333        """
     334        Form should advance if the hash is present and good, as calculated using
     335        django 1.3 method.
     336        """
     337        data = {"0-field": "test",
     338                "1-field": "test2",
     339                "hash_0": "d662b87b56e906966801a96e49aed87559a8538e",
     340                "wizard_step": "1"}
     341        response = self.client.post('/wizard/', data)
     342        self.assertEquals(2, response.context['step0'])
  • new file django/contrib/formtools/tests/templates/formwizard/wizard.html

    diff -r a7ef72553337 django/contrib/formtools/tests/templates/formwizard/wizard.html
    - +  
     1<p>Step {{ step }} of {{ step_count }}</p>
     2<form action="." method="post">{% csrf_token %}
     3<table>
     4{{ form }}
     5</table>
     6<input type="hidden" name="{{ step_field }}" value="{{ step0 }}" />
     7{{ previous_fields|safe }}
     8<input type="submit">
     9</form>
  • new file django/contrib/formtools/tests/urls.py

    diff -r a7ef72553337 django/contrib/formtools/tests/urls.py
    - +  
     1"""
     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 a7ef72553337 django/contrib/formtools/utils.py
    a b  
    33except ImportError:
    44    import pickle
    55
     6import hmac
     7
    68from django.conf import settings
    7 from django.utils.hashcompat import md5_constructor
     9from django.utils.hashcompat import md5_constructor, sha_hmac
    810from django.forms import BooleanField
    911
     12
    1013def security_hash(request, form, *args):
    1114    """
    1215    Calculates a security hash for the given Form instance.
     
    1518    order, pickles the result with the SECRET_KEY setting, then takes an md5
    1619    hash of that.
    1720    """
    18 
     21    import warnings
     22    warnings.warn("security_hash is deprecated; use form_hmac instead",
     23                  PendingDeprecationWarning)
    1924    data = []
    2025    for bf in form:
    2126        # Get the value from the form data. If the form allows empty or hasn't
     
    3742
    3843    return md5_constructor(pickled).hexdigest()
    3944
     45
     46def form_hmac(form):
     47    """
     48    Calculates a security hash for the given Form instance.  'key_extra' should
     49    be a string that is unique to a particular form wizard.
     50    """
     51    data = []
     52    for bf in form:
     53        # Get the value from the form data. If the form allows empty or hasn't
     54        # changed then don't call clean() to avoid trigger validation errors.
     55        if form.empty_permitted and not form.has_changed():
     56            value = bf.data or ''
     57        else:
     58            value = bf.field.clean(bf.data) or ''
     59        if isinstance(value, basestring):
     60            value = value.strip()
     61        data.append((bf.name, value))
     62
     63    pickled = pickle.dumps(data, pickle.HIGHEST_PROTOCOL)
     64    key = 'django.contrib.formtools' + settings.SECRET_KEY
     65    return hmac.new(key, pickled, sha_hmac).hexdigest()
  • django/contrib/formtools/wizard.py

    diff -r a7ef72553337 django/contrib/formtools/wizard.py
    a b  
    1313from django.template.context import RequestContext
    1414from django.utils.hashcompat import md5_constructor
    1515from django.utils.translation import ugettext_lazy as _
    16 from django.contrib.formtools.utils import security_hash
     16from django.contrib.formtools.utils import security_hash, form_hmac
    1717from django.utils.decorators import method_decorator
     18from django.utils.text import constant_time_compare
    1819from django.views.decorators.csrf import csrf_protect
    1920
    2021
     
    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 a7ef72553337 django/contrib/messages/storage/cookie.py
    a b  
    66from django.http import CompatCookie
    77from django.utils import simplejson as json
    88from django.utils.hashcompat import sha_hmac
     9from django.utils.text import constant_time_compare
    910
    1011
    1112class MessageEncoder(json.JSONEncoder):
     
    139140        bits = data.split('$', 1)
    140141        if len(bits) == 2:
    141142            hash, value = bits
    142             if hash == self._hash(value):
     143            if constant_time_compare(hash, self._hash(value)):
    143144                try:
    144145                    # If we get here (and the JSON decode works), everything is
    145146                    # good. In any other case, drop back and return None.
  • django/contrib/sessions/backends/base.py

    diff -r a7ef72553337 django/contrib/sessions/backends/base.py
    a b  
    11import base64
     2import hmac
    23import os
    34import random
    45import sys
     
    1112
    1213from django.conf import settings
    1314from django.core.exceptions import SuspiciousOperation
    14 from django.utils.hashcompat import md5_constructor
     15from django.utils.hashcompat import md5_constructor, sha_hmac
     16from django.utils.text import constant_time_compare
    1517
    1618# Use the system (hardware-based) random number generator if it exists.
    1719if hasattr(random, 'SystemRandom'):
     
    8385    def delete_test_cookie(self):
    8486        del self[self.TEST_COOKIE_NAME]
    8587
     88    def _hash(self, value):
     89        key = "django.contrib.sessions" + self.__class__.__name__ + settings.SECRET_KEY
     90        return hmac.new(key, value, sha_hmac).hexdigest()
     91
    8692    def encode(self, session_dict):
    8793        "Returns the given session dictionary pickled and encoded as a string."
    8894        pickled = pickle.dumps(session_dict, pickle.HIGHEST_PROTOCOL)
    89         pickled_md5 = md5_constructor(pickled + settings.SECRET_KEY).hexdigest()
    90         return base64.encodestring(pickled + pickled_md5)
     95        hash = self._hash(pickled)
     96        return base64.encodestring(hash + ":" + pickled)
    9197
    9298    def decode(self, session_data):
    9399        encoded_data = base64.decodestring(session_data)
     100        try:
     101            # could produce ValueError if there is no ':'
     102            hash, pickled = encoded_data.split(':', 1)
     103            expected_hash = self._hash(pickled)
     104            if not constant_time_compare(hash, expected_hash):
     105                raise SuspiciousOperation("Session data corrupted")
     106            else:
     107                return pickle.loads(pickled)
     108        except Exception:
     109            # ValueError, SuspiciousOperation, unpickling exceptions
     110            # Fall back to Django 1.2 method
     111            # PendingDeprecationWarning <- here to remind us to
     112            # remove this fallback in Django 1.5
     113            try:
     114                return self._decode_old(session_data)
     115            except Exception:
     116                # Unpickling can cause a variety of exceptions. If something happens,
     117                # just return an empty dictionary (an empty session).
     118                return {}
     119
     120    def _decode_old(self, session_data):
     121        encoded_data = base64.decodestring(session_data)
    94122        pickled, tamper_check = encoded_data[:-32], encoded_data[-32:]
    95         if md5_constructor(pickled + settings.SECRET_KEY).hexdigest() != tamper_check:
     123        if not constant_time_compare(md5_constructor(pickled + settings.SECRET_KEY).hexdigest(),
     124                                     tamper_check):
    96125            raise SuspiciousOperation("User tampered with session cookie.")
    97         try:
    98             return pickle.loads(pickled)
    99         # Unpickling can cause a variety of exceptions. If something happens,
    100         # just return an empty dictionary (an empty session).
    101         except:
    102             return {}
     126        return pickle.loads(pickled)
    103127
    104128    def update(self, dict_):
    105129        self._session.update(dict_)
  • django/contrib/sessions/tests.py

    diff -r a7ef72553337 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 a7ef72553337 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.text 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),
  • django/utils/text.py

    diff -r a7ef72553337 django/utils/text.py
    a b  
    280280    quote = s[0]
    281281    return s[1:-1].replace(r'\%s' % quote, quote).replace(r'\\', '\\')
    282282unescape_string_literal = allow_lazy(unescape_string_literal)
     283
     284def constant_time_compare(val1, val2):
     285    """
     286    Returns True if the two strings are equal, False otherwise.
     287
     288    The time taken is independent of the number of characters that match.
     289    """
     290    if len(val1) != len(val2):
     291        return False
     292    result = 0
     293    for x, y in zip(val1, val2):
     294        result |= ord(x) ^ ord(y)
     295    return result == 0
  • docs/internals/deprecation.txt

    diff -r a7ef72553337 docs/internals/deprecation.txt
    a b  
    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 a7ef72553337 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 a7ef72553337 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