1
0
mirror of https://github.com/django/django.git synced 2025-06-05 03:29:12 +00:00

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

Co-authored-by: Craig Anderson <craiga@craiga.id.au>
This commit is contained in:
Claude Paroz 2020-01-30 23:11:09 +01:00 committed by Mariusz Felisiak
parent bcc9fa2528
commit 8ae84156d6
4 changed files with 48 additions and 12 deletions

View File

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

View File

@ -46,6 +46,9 @@ details on these changes.
* The ``HttpRequest.is_ajax()`` method will be removed. * 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 See the :ref:`Django 3.1 release notes <deprecated-features-3.1>` for more
details on these changes. details on these changes.

View File

@ -482,6 +482,11 @@ Miscellaneous
the new :meth:`.HttpRequest.accepts` method if your code depends on the the new :meth:`.HttpRequest.accepts` method if your code depends on the
client ``Accept`` HTTP header. 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: .. _removed-features-3.1:
Features removed in 3.1 Features removed in 3.1

View File

@ -153,3 +153,14 @@ class CookieTests(BaseTests, SimpleTestCase):
storage = self.get_storage() storage = self.get_storage()
self.assertIsInstance(encode_decode(mark_safe("<b>Hello Django!</b>")), SafeData) self.assertIsInstance(encode_decode(mark_safe("<b>Hello Django!</b>")), SafeData)
self.assertNotIsInstance(encode_decode("<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)