Refs #31274 -- Removed support for the pre-Django 3.1 encoding format of sessions.

Per deprecation timeline.
This commit is contained in:
Mariusz Felisiak 2021-01-11 21:27:01 +01:00
parent 66b4046d68
commit 8250145a0c
3 changed files with 15 additions and 70 deletions

View File

@ -1,16 +1,11 @@
import base64
import logging import logging
import string import string
from datetime import datetime, timedelta from datetime import datetime, timedelta
from django.conf import settings from django.conf import settings
from django.contrib.sessions.exceptions import SuspiciousSession
from django.core import signing from django.core import signing
from django.core.exceptions import SuspiciousOperation
from django.utils import timezone from django.utils import timezone
from django.utils.crypto import ( from django.utils.crypto import get_random_string
constant_time_compare, get_random_string, salted_hmac,
)
from django.utils.module_loading import import_string from django.utils.module_loading import import_string
# session_key should not be case sensitive because some backends can store it # session_key should not be case sensitive because some backends can store it
@ -91,16 +86,8 @@ class SessionBase:
def delete_test_cookie(self): def delete_test_cookie(self):
del self[self.TEST_COOKIE_NAME] del self[self.TEST_COOKIE_NAME]
def _hash(self, value):
# RemovedInDjango40Warning: pre-Django 3.1 format will be invalid.
key_salt = "django.contrib.sessions" + self.__class__.__name__
return salted_hmac(key_salt, value).hexdigest()
def encode(self, session_dict): def encode(self, session_dict):
"Return the given session dictionary serialized and encoded as a string." "Return the given session dictionary serialized and encoded as a string."
# RemovedInDjango40Warning: DEFAULT_HASHING_ALGORITHM will be removed.
if settings.DEFAULT_HASHING_ALGORITHM == 'sha1':
return self._legacy_encode(session_dict)
return signing.dumps( return signing.dumps(
session_dict, salt=self.key_salt, serializer=self.serializer, session_dict, salt=self.key_salt, serializer=self.serializer,
compress=True, compress=True,
@ -109,44 +96,14 @@ class SessionBase:
def decode(self, session_data): def decode(self, session_data):
try: try:
return signing.loads(session_data, salt=self.key_salt, serializer=self.serializer) return signing.loads(session_data, salt=self.key_salt, serializer=self.serializer)
# RemovedInDjango40Warning: when the deprecation ends, handle here
# exceptions similar to what _legacy_decode() does now.
except signing.BadSignature: except signing.BadSignature:
try: logger = logging.getLogger('django.security.SuspiciousSession')
# Return an empty session if data is not in the pre-Django 3.1 logger.warning('Session data corrupted')
# format.
return self._legacy_decode(session_data)
except Exception:
logger = logging.getLogger('django.security.SuspiciousSession')
logger.warning('Session data corrupted')
return {}
except Exception: except Exception:
return self._legacy_decode(session_data) # ValueError, unpickling exceptions. If any of these happen, just
# return an empty dictionary (an empty session).
def _legacy_encode(self, session_dict): pass
# RemovedInDjango40Warning. return {}
serialized = self.serializer().dumps(session_dict)
hash = self._hash(serialized)
return base64.b64encode(hash.encode() + b':' + serialized).decode('ascii')
def _legacy_decode(self, session_data):
# RemovedInDjango40Warning: pre-Django 3.1 format will be invalid.
encoded_data = base64.b64decode(session_data.encode('ascii'))
try:
# could produce ValueError if there is no ':'
hash, serialized = encoded_data.split(b':', 1)
expected_hash = self._hash(serialized)
if not constant_time_compare(hash.decode(), expected_hash):
raise SuspiciousSession("Session data corrupted")
else:
return self.serializer().loads(serialized)
except Exception as e:
# ValueError, SuspiciousOperation, unpickling exceptions. If any of
# these happen, just return an empty dictionary (an empty session).
if isinstance(e, SuspiciousOperation):
logger = logging.getLogger('django.security.%s' % e.__class__.__name__)
logger.warning(str(e))
return {}
def update(self, dict_): def update(self, dict_):
self._session.update(dict_) self._session.update(dict_)

View File

@ -283,3 +283,5 @@ to remove usage of these features.
* Support for the pre-Django 3.1 password reset tokens in the admin site (that * Support for the pre-Django 3.1 password reset tokens in the admin site (that
use the SHA-1 hashing algorithm) is removed. use the SHA-1 hashing algorithm) is removed.
* Support for the pre-Django 3.1 encoding format of sessions is removed.

View File

@ -31,13 +31,13 @@ from django.core import management
from django.core.cache import caches from django.core.cache import caches
from django.core.cache.backends.base import InvalidCacheBackendError from django.core.cache.backends.base import InvalidCacheBackendError
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.core.signing import TimestampSigner
from django.http import HttpResponse from django.http import HttpResponse
from django.test import ( from django.test import (
RequestFactory, SimpleTestCase, TestCase, ignore_warnings, RequestFactory, SimpleTestCase, TestCase, ignore_warnings,
override_settings, override_settings,
) )
from django.utils import timezone from django.utils import timezone
from django.utils.deprecation import RemovedInDjango40Warning
from .models import SessionStore as CustomDatabaseSession from .models import SessionStore as CustomDatabaseSession
@ -315,25 +315,6 @@ class SessionTestsMixin:
encoded = self.session.encode(data) encoded = self.session.encode(data)
self.assertEqual(self.session.decode(encoded), data) self.assertEqual(self.session.decode(encoded), data)
@override_settings(SECRET_KEY='django_tests_secret_key')
def test_decode_legacy(self):
# RemovedInDjango40Warning: pre-Django 3.1 sessions will be invalid.
legacy_encoded = (
'OWUzNTNmNWQxNTBjOWExZmM4MmQ3NzNhMDRmMjU4NmYwNDUyNGI2NDp7ImEgdGVzd'
'CBrZXkiOiJhIHRlc3QgdmFsdWUifQ=='
)
self.assertEqual(
self.session.decode(legacy_encoded),
{'a test key': 'a test value'},
)
@ignore_warnings(category=RemovedInDjango40Warning)
def test_default_hashing_algorith_legacy_decode(self):
with self.settings(DEFAULT_HASHING_ALGORITHM='sha1'):
data = {'a test key': 'a test value'}
encoded = self.session.encode(data)
self.assertEqual(self.session._legacy_decode(encoded), data)
def test_decode_failure_logged_to_security(self): def test_decode_failure_logged_to_security(self):
tests = [ tests = [
base64.b64encode(b'flaskdj:alkdjf').decode('ascii'), base64.b64encode(b'flaskdj:alkdjf').decode('ascii'),
@ -346,6 +327,11 @@ class SessionTestsMixin:
# The failed decode is logged. # The failed decode is logged.
self.assertIn('Session data corrupted', cm.output[0]) self.assertIn('Session data corrupted', cm.output[0])
def test_decode_serializer_exception(self):
signer = TimestampSigner(salt=self.session.key_salt)
encoded = signer.sign(b'invalid data')
self.assertEqual(self.session.decode(encoded), {})
def test_actual_expiry(self): def test_actual_expiry(self):
# this doesn't work with JSONSerializer (serializing timedelta) # this doesn't work with JSONSerializer (serializing timedelta)
with override_settings(SESSION_SERIALIZER='django.contrib.sessions.serializers.PickleSerializer'): with override_settings(SESSION_SERIALIZER='django.contrib.sessions.serializers.PickleSerializer'):