From 54646a423b4501aeb80bbdd9238f20500c84cd5f Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Wed, 29 Apr 2020 16:45:00 +0200 Subject: [PATCH] Refs #27468 -- Made user sessions use SHA-256 algorithm. --- django/contrib/auth/__init__.py | 9 +++++++-- django/contrib/auth/base_user.py | 7 ++++++- docs/internals/deprecation.txt | 3 +++ docs/releases/3.1.txt | 4 ++++ docs/topics/auth/customizing.txt | 4 ++++ tests/auth_tests/test_middleware.py | 11 +++++++++++ tests/auth_tests/test_views.py | 23 ++++++++++++++++++++++- 7 files changed, 57 insertions(+), 4 deletions(-) diff --git a/django/contrib/auth/__init__.py b/django/contrib/auth/__init__.py index 09db690b5c..de2aa785e6 100644 --- a/django/contrib/auth/__init__.py +++ b/django/contrib/auth/__init__.py @@ -187,8 +187,13 @@ def get_user(request): user.get_session_auth_hash() ) if not session_hash_verified: - request.session.flush() - user = None + if not ( + session_hash and + hasattr(user, '_legacy_get_session_auth_hash') and + constant_time_compare(session_hash, user._legacy_get_session_auth_hash()) + ): + request.session.flush() + user = None return user or AnonymousUser() diff --git a/django/contrib/auth/base_user.py b/django/contrib/auth/base_user.py index f39c12a350..bb51cfbcc9 100644 --- a/django/contrib/auth/base_user.py +++ b/django/contrib/auth/base_user.py @@ -120,12 +120,17 @@ class AbstractBaseUser(models.Model): """ return is_password_usable(self.password) + def _legacy_get_session_auth_hash(self): + # RemovedInDjango40Warning: pre-Django 3.1 hashes will be invalid. + key_salt = 'django.contrib.auth.models.AbstractBaseUser.get_session_auth_hash' + return salted_hmac(key_salt, self.password, algorithm='sha1').hexdigest() + def get_session_auth_hash(self): """ Return an HMAC of the password field. """ key_salt = "django.contrib.auth.models.AbstractBaseUser.get_session_auth_hash" - return salted_hmac(key_salt, self.password).hexdigest() + return salted_hmac(key_salt, self.password, algorithm='sha256').hexdigest() @classmethod def get_email_field_name(cls): diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 8d3cc62d90..95b0c5a3a1 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -57,6 +57,9 @@ details on these changes. * Support for the pre-Django 3.1 ``django.core.signing.Signer`` signatures (encoded with the SHA-1 algorithm) will be removed. +* Support for the pre-Django 3.1 user sessions (that use the SHA-1 algorithm) + will be removed. + * The ``get_request`` argument for ``django.utils.deprecation.MiddlewareMixin.__init__()`` will be required and won't accept ``None``. diff --git a/docs/releases/3.1.txt b/docs/releases/3.1.txt index 16cb9c4e6e..1de4f24684 100644 --- a/docs/releases/3.1.txt +++ b/docs/releases/3.1.txt @@ -98,6 +98,10 @@ Minor features * The password reset mechanism now uses the SHA-256 hashing algorithm. Support for tokens that use the old hashing algorithm remains until Django 4.0. +* :meth:`.AbstractBaseUser.get_session_auth_hash` now uses the SHA-256 hashing + algorithm. Support for user sessions that use the old hashing algorithm + remains until Django 4.0. + :mod:`django.contrib.contenttypes` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/topics/auth/customizing.txt b/docs/topics/auth/customizing.txt index c8a9a39158..6b816c42fd 100644 --- a/docs/topics/auth/customizing.txt +++ b/docs/topics/auth/customizing.txt @@ -713,6 +713,10 @@ The following attributes and methods are available on any subclass of Returns an HMAC of the password field. Used for :ref:`session-invalidation-on-password-change`. + .. versionchanged:: 3.1 + + The hashing algorithm was changed to the SHA-256. + :class:`~models.AbstractUser` subclasses :class:`~models.AbstractBaseUser`: .. class:: models.AbstractUser diff --git a/tests/auth_tests/test_middleware.py b/tests/auth_tests/test_middleware.py index 3c31475d27..5538225acb 100644 --- a/tests/auth_tests/test_middleware.py +++ b/tests/auth_tests/test_middleware.py @@ -1,3 +1,4 @@ +from django.contrib.auth import HASH_SESSION_KEY from django.contrib.auth.middleware import AuthenticationMiddleware from django.contrib.auth.models import User from django.http import HttpRequest, HttpResponse @@ -18,6 +19,16 @@ class TestAuthenticationMiddleware(TestCase): self.assertIsNotNone(self.request.user) self.assertFalse(self.request.user.is_anonymous) + def test_no_password_change_does_not_invalidate_legacy_session(self): + # RemovedInDjango40Warning: pre-Django 3.1 hashes will be invalid. + session = self.client.session + session[HASH_SESSION_KEY] = self.user._legacy_get_session_auth_hash() + session.save() + self.request.session = session + self.middleware(self.request) + self.assertIsNotNone(self.request.user) + self.assertFalse(self.request.user.is_anonymous) + def test_changed_password_invalidates_session(self): # After password change, user should be anonymous self.user.set_password('new_password') diff --git a/tests/auth_tests/test_views.py b/tests/auth_tests/test_views.py index f33cbc8382..48278e23f9 100644 --- a/tests/auth_tests/test_views.py +++ b/tests/auth_tests/test_views.py @@ -10,7 +10,7 @@ from django.apps import apps from django.conf import settings from django.contrib.admin.models import LogEntry from django.contrib.auth import ( - BACKEND_SESSION_KEY, REDIRECT_FIELD_NAME, SESSION_KEY, + BACKEND_SESSION_KEY, HASH_SESSION_KEY, REDIRECT_FIELD_NAME, SESSION_KEY, ) from django.contrib.auth.forms import ( AuthenticationForm, PasswordChangeForm, SetPasswordForm, @@ -711,6 +711,27 @@ class LoginTest(AuthViewsTestCase): self.login(password='foobar') self.assertNotEqual(original_session_key, self.client.session.session_key) + def test_legacy_session_key_flushed_on_login(self): + # RemovedInDjango40Warning. + user = User.objects.get(username='testclient') + engine = import_module(settings.SESSION_ENGINE) + session = engine.SessionStore() + session[SESSION_KEY] = user.id + session[HASH_SESSION_KEY] = user._legacy_get_session_auth_hash() + session.save() + original_session_key = session.session_key + self.client.cookies[settings.SESSION_COOKIE_NAME] = original_session_key + # Legacy session key is flushed on login. + self.login() + self.assertNotEqual(original_session_key, self.client.session.session_key) + # Legacy session key is flushed after a password change. + user.set_password('password_2') + user.save() + original_session_key = session.session_key + self.client.cookies[settings.SESSION_COOKIE_NAME] = original_session_key + self.login(password='password_2') + self.assertNotEqual(original_session_key, self.client.session.session_key) + def test_login_session_without_hash_session_key(self): """ Session without django.contrib.auth.HASH_SESSION_KEY should login