Browse Source

Refs #27468 -- Made user sessions use SHA-256 algorithm.

Mariusz Felisiak 4 years ago
parent
commit
54646a423b

+ 7 - 2
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()
 

+ 6 - 1
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):

+ 3 - 0
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``.

+ 4 - 0
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`
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 

+ 4 - 0
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

+ 11 - 0
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')

+ 22 - 1
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