diff --git a/formtools/utils.py b/formtools/utils.py index 033541f..55aadda 100644 --- a/formtools/utils.py +++ b/formtools/utils.py @@ -1,9 +1,30 @@ import pickle from django.core.files.uploadedfile import TemporaryUploadedFile +from django.db.models import QuerySet from django.utils.crypto import salted_hmac +def sanitise(obj): + if isinstance(obj, list): + return [sanitise(o) for o in obj] + elif isinstance(obj, tuple): + return tuple([sanitise(o) for o in obj]) + elif isinstance(obj, QuerySet): + return [sanitise(o) for o in list(obj)] + try: + od = obj.__dict__ + nd = {'_class': obj.__class__} + for key, val in od.items(): + if not key.startswith('_'): + # ignore Django internal attributes + nd[key] = sanitise(val) + return nd + except Exception: + pass + return obj + + def form_hmac(form): """ Calculates a security hash for the given Form instance. @@ -22,6 +43,7 @@ def form_hmac(form): value = value.read() data.append((bf.name, value)) - pickled = pickle.dumps(data, pickle.HIGHEST_PROTOCOL) + sanitised_data = sanitise(data) + pickled = pickle.dumps(sanitised_data, pickle.HIGHEST_PROTOCOL) key_salt = 'django.contrib.formtools' return salted_hmac(key_salt, pickled).hexdigest() diff --git a/tests/forms.py b/tests/forms.py index 85534f5..714723d 100644 --- a/tests/forms.py +++ b/tests/forms.py @@ -1,4 +1,32 @@ from django import forms +from django.db import models + + +class ManyModel(models.Model): + name = models.CharField(max_length=100) + + class Meta: + app_label = 'formtools' + + def __str__(self): + return self.name + + +class OtherModel(models.Model): + name = models.CharField(max_length=100) + manymodels = models.ManyToManyField(ManyModel) + + class Meta: + app_label = 'formtools' + + def __str__(self): + return self.name + " with " + ", ".join(map(str, self.manymodels.all())) + + +class OtherModelForm(forms.ModelForm): + class Meta: + model = OtherModel + fields = ["name", "manymodels"] class TestForm(forms.Form): diff --git a/tests/tests.py b/tests/tests.py index f9a2a35..7c8f37d 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -13,7 +13,8 @@ from formtools import preview, utils from .forms import ( - HashTestBlankForm, HashTestForm, HashTestFormWithFile, TestForm, + HashTestBlankForm, HashTestForm, HashTestFormWithFile, ManyModel, + OtherModelForm, TestForm, ) success_string = "Done was called!" @@ -214,3 +215,28 @@ def test_hash_with_file(self): hash1 = utils.form_hmac(f1) hash2 = utils.form_hmac(f2) self.assertNotEqual(hash1, hash2) + + +class PicklingTests(unittest.TestCase): + + def setUp(self): + super().setUp() + ManyModel.objects.create(name="jane") + + def test_queryset_hash(self): + """ + Regression test for #10034: the hash generation function should ignore + leading/trailing whitespace so as to be friendly to broken browsers that + submit it (usually in textareas). + """ + + qs1 = ManyModel.objects.all() + qs2 = ManyModel.objects.all() + + qs1._prefetch_done = True + qs2._prefetch_done = False + f1 = OtherModelForm({'name': 'joe', 'manymodels': qs1}) + f2 = OtherModelForm({'name': 'joe', 'manymodels': qs2}) + hash1 = utils.form_hmac(f1) + hash2 = utils.form_hmac(f2) + self.assertEqual(hash1, hash2)