Browse Source

Fixed #27604 -- Used the cookie signer to sign message cookies.

Co-authored-by: Craig Anderson <craiga@craiga.id.au>
Claude Paroz 5 years ago
parent
commit
8ae84156d6

+ 29 - 12
django/contrib/messages/storage/cookie.py

@@ -2,6 +2,7 @@ import json
 
 from django.conf import settings
 from django.contrib.messages.storage.base import BaseStorage, Message
+from django.core import signing
 from django.http import SimpleCookie
 from django.utils.crypto import constant_time_compare, salted_hmac
 from django.utils.safestring import SafeData, mark_safe
@@ -58,6 +59,10 @@ class CookieStorage(BaseStorage):
     not_finished = '__messagesnotfinished__'
     key_salt = 'django.contrib.messages'
 
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.signer = signing.get_cookie_signer(salt=self.key_salt)
+
     def _get(self, *args, **kwargs):
         """
         Retrieve a list of messages from the messages cookie. If the
@@ -118,8 +123,9 @@ class CookieStorage(BaseStorage):
         self._update_cookie(encoded_data, response)
         return unstored_messages
 
-    def _hash(self, value):
+    def _legacy_hash(self, value):
         """
+        # RemovedInDjango40Warning: pre-Django 3.1 hashes will be invalid.
         Create an HMAC/SHA1 hash based on the value and the project setting's
         SECRET_KEY, modified to make it unique for the present purpose.
         """
@@ -136,7 +142,7 @@ class CookieStorage(BaseStorage):
         if messages or encode_empty:
             encoder = MessageEncoder(separators=(',', ':'))
             value = encoder.encode(messages)
-            return '%s$%s' % (self._hash(value), value)
+            return self.signer.sign(value)
 
     def _decode(self, data):
         """
@@ -147,17 +153,28 @@ class CookieStorage(BaseStorage):
         """
         if not data:
             return None
-        bits = data.split('$', 1)
-        if len(bits) == 2:
-            hash, value = bits
-            if constant_time_compare(hash, self._hash(value)):
-                try:
-                    # If we get here (and the JSON decode works), everything is
-                    # good. In any other case, drop back and return None.
-                    return json.loads(value, cls=MessageDecoder)
-                except json.JSONDecodeError:
-                    pass
+        try:
+            decoded = self.signer.unsign(data)
+        except signing.BadSignature:
+            # RemovedInDjango40Warning: when the deprecation ends, replace
+            # with:
+            #   decoded = None.
+            decoded = self._legacy_decode(data)
+        if decoded:
+            try:
+                return json.loads(decoded, cls=MessageDecoder)
+            except json.JSONDecodeError:
+                pass
         # Mark the data as used (so it gets removed) since something was wrong
         # with the data.
         self.used = True
         return None
+
+    def _legacy_decode(self, data):
+        # RemovedInDjango40Warning: pre-Django 3.1 hashes will be invalid.
+        bits = data.split('$', 1)
+        if len(bits) == 2:
+            hash_, value = bits
+            if constant_time_compare(hash_, self._legacy_hash(value)):
+                return value
+        return None

+ 3 - 0
docs/internals/deprecation.txt

@@ -46,6 +46,9 @@ details on these changes.
 
 * The ``HttpRequest.is_ajax()`` method will be removed.
 
+* Support for the pre-Django 3.1 encoding format of cookies values used by
+  ``django.contrib.messages.storage.cookie.CookieStorage`` will be removed.
+
 See the :ref:`Django 3.1 release notes <deprecated-features-3.1>` for more
 details on these changes.
 

+ 5 - 0
docs/releases/3.1.txt

@@ -482,6 +482,11 @@ Miscellaneous
   the new :meth:`.HttpRequest.accepts` method if your code depends on the
   client ``Accept`` HTTP header.
 
+* The encoding format of cookies values used by
+  :class:`~django.contrib.messages.storage.cookie.CookieStorage` is different
+  from the format generated by older versions of Django. Support for the old
+  format remains until Django 4.0.
+
 .. _removed-features-3.1:
 
 Features removed in 3.1

+ 11 - 0
tests/messages_tests/test_cookie.py

@@ -153,3 +153,14 @@ class CookieTests(BaseTests, SimpleTestCase):
         storage = self.get_storage()
         self.assertIsInstance(encode_decode(mark_safe("<b>Hello Django!</b>")), SafeData)
         self.assertNotIsInstance(encode_decode("<b>Hello Django!</b>"), SafeData)
+
+    def test_legacy_hash_decode(self):
+        # RemovedInDjango40Warning: pre-Django 3.1 hashes will be invalid.
+        storage = self.storage_class(self.get_request())
+        messages = ['this', 'that']
+        # Encode/decode a message using the pre-Django 3.1 hash.
+        encoder = MessageEncoder(separators=(',', ':'))
+        value = encoder.encode(messages)
+        encoded_messages = '%s$%s' % (storage._legacy_hash(value), value)
+        decoded_messages = storage._decode(encoded_messages)
+        self.assertEqual(messages, decoded_messages)