Refs #31274 -- Removed support for the pre-Django 3.1 encoding format of sessions.
Per deprecation timeline.
This commit is contained in:
parent
66b4046d68
commit
8250145a0c
|
@ -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_)
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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'):
|
||||||
|
|
Loading…
Reference in New Issue