mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	Fixed #28718 -- Allowed user to request a password reset if their password doesn't use an enabled hasher.
Regression inaeb1389442. Reverted changes to is_password_usable() from703c266682and documentation changes from92f48680db.
This commit is contained in:
		@@ -116,9 +116,7 @@ class AbstractBaseUser(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def has_usable_password(self):
 | 
					    def has_usable_password(self):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Return False if set_unusable_password() has been called for this user,
 | 
					        Return False if set_unusable_password() has been called for this user.
 | 
				
			||||||
        or if the password is None, or if the password uses a hasher that's not
 | 
					 | 
				
			||||||
        in the PASSWORD_HASHERS setting.
 | 
					 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return is_password_usable(self.password)
 | 
					        return is_password_usable(self.password)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -21,13 +21,11 @@ UNUSABLE_PASSWORD_SUFFIX_LENGTH = 40  # number of random chars to add after UNUS
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def is_password_usable(encoded):
 | 
					def is_password_usable(encoded):
 | 
				
			||||||
    if encoded is None or encoded.startswith(UNUSABLE_PASSWORD_PREFIX):
 | 
					    """
 | 
				
			||||||
        return False
 | 
					    Return True if this password wasn't generated by
 | 
				
			||||||
    try:
 | 
					    User.set_unusable_password(), i.e. make_password(None).
 | 
				
			||||||
        identify_hasher(encoded)
 | 
					    """
 | 
				
			||||||
    except ValueError:
 | 
					    return encoded is None or not encoded.startswith(UNUSABLE_PASSWORD_PREFIX)
 | 
				
			||||||
        return False
 | 
					 | 
				
			||||||
    return True
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def check_password(password, encoded, setter=None, preferred='default'):
 | 
					def check_password(password, encoded, setter=None, preferred='default'):
 | 
				
			||||||
@@ -42,7 +40,11 @@ def check_password(password, encoded, setter=None, preferred='default'):
 | 
				
			|||||||
        return False
 | 
					        return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    preferred = get_hasher(preferred)
 | 
					    preferred = get_hasher(preferred)
 | 
				
			||||||
    hasher = identify_hasher(encoded)
 | 
					    try:
 | 
				
			||||||
 | 
					        hasher = identify_hasher(encoded)
 | 
				
			||||||
 | 
					    except ValueError:
 | 
				
			||||||
 | 
					        # encoded is gibberish or uses a hasher that's no longer installed.
 | 
				
			||||||
 | 
					        return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    hasher_changed = hasher.algorithm != preferred.algorithm
 | 
					    hasher_changed = hasher.algorithm != preferred.algorithm
 | 
				
			||||||
    must_update = hasher_changed or preferred.must_update(encoded)
 | 
					    must_update = hasher_changed or preferred.must_update(encoded)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -212,9 +212,15 @@ Methods
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        Returns ``False`` if
 | 
					        Returns ``False`` if
 | 
				
			||||||
        :meth:`~django.contrib.auth.models.User.set_unusable_password()` has
 | 
					        :meth:`~django.contrib.auth.models.User.set_unusable_password()` has
 | 
				
			||||||
        been called for this user, or if the password is ``None``, or if the
 | 
					        been called for this user.
 | 
				
			||||||
        password uses a hasher that's not in the :setting:`PASSWORD_HASHERS`
 | 
					
 | 
				
			||||||
        setting.
 | 
					        .. versionchanged:: 2.1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            In older versions, this also returns ``False`` if the password is
 | 
				
			||||||
 | 
					            ``None`` or an empty string, or if the password uses a hasher
 | 
				
			||||||
 | 
					            that's not in the :setting:`PASSWORD_HASHERS` setting. That
 | 
				
			||||||
 | 
					            behavior is considered a bug as it prevents users with such
 | 
				
			||||||
 | 
					            passwords from requesting a password reset.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    .. method:: get_group_permissions(obj=None)
 | 
					    .. method:: get_group_permissions(obj=None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -358,6 +358,14 @@ Miscellaneous
 | 
				
			|||||||
  changed from 0 to an empty string, which mainly may require some adjustments
 | 
					  changed from 0 to an empty string, which mainly may require some adjustments
 | 
				
			||||||
  in tests that compare HTML.
 | 
					  in tests that compare HTML.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					* :meth:`.User.has_usable_password` and the
 | 
				
			||||||
 | 
					  :func:`~django.contrib.auth.hashers.is_password_usable` function no longer
 | 
				
			||||||
 | 
					  return ``False`` if the password is ``None`` or an empty string, or if the
 | 
				
			||||||
 | 
					  password uses a hasher that's not in the :setting:`PASSWORD_HASHERS` setting.
 | 
				
			||||||
 | 
					  This undocumented behavior was a regression in Django 1.6 and prevented users
 | 
				
			||||||
 | 
					  with such passwords from requesting a password reset. Audit your code to
 | 
				
			||||||
 | 
					  confirm that your usage of these APIs don't rely on the old behavior.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.. _deprecated-features-2.1:
 | 
					.. _deprecated-features-2.1:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Features deprecated in 2.1
 | 
					Features deprecated in 2.1
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -409,8 +409,16 @@ from the ``User`` model.
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
.. function:: is_password_usable(encoded_password)
 | 
					.. function:: is_password_usable(encoded_password)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
   Checks if the given string is a hashed password that has a chance
 | 
					    Returns ``False`` if the password is a result of
 | 
				
			||||||
   of being verified against :func:`check_password`.
 | 
					    :meth:`.User.set_unusable_password`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .. versionchanged:: 2.1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        In older versions, this also returns ``False`` if the password is
 | 
				
			||||||
 | 
					        ``None`` or an empty string, or if the password uses a hasher that's
 | 
				
			||||||
 | 
					        not in the :setting:`PASSWORD_HASHERS` setting. That behavior is
 | 
				
			||||||
 | 
					        considered a bug as it prevents users with such passwords from
 | 
				
			||||||
 | 
					        requesting a password reset.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.. _password-validation:
 | 
					.. _password-validation:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -276,9 +276,11 @@ class TestUtilsHashPass(SimpleTestCase):
 | 
				
			|||||||
        with self.assertRaisesMessage(ValueError, msg % 'lolcat'):
 | 
					        with self.assertRaisesMessage(ValueError, msg % 'lolcat'):
 | 
				
			||||||
            identify_hasher('lolcat$salt$hash')
 | 
					            identify_hasher('lolcat$salt$hash')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_bad_encoded(self):
 | 
					    def test_is_password_usable(self):
 | 
				
			||||||
        self.assertFalse(is_password_usable('lètmein_badencoded'))
 | 
					        passwords = ('lètmein_badencoded', '', None)
 | 
				
			||||||
        self.assertFalse(is_password_usable(''))
 | 
					        for password in passwords:
 | 
				
			||||||
 | 
					            with self.subTest(password=password):
 | 
				
			||||||
 | 
					                self.assertIs(is_password_usable(password), True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_low_level_pbkdf2(self):
 | 
					    def test_low_level_pbkdf2(self):
 | 
				
			||||||
        hasher = PBKDF2PasswordHasher()
 | 
					        hasher = PBKDF2PasswordHasher()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -158,6 +158,13 @@ class UserManagerTestCase(TestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
class AbstractBaseUserTests(TestCase):
 | 
					class AbstractBaseUserTests(TestCase):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_has_usable_password(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Passwords are usable even if they don't correspond to a hasher in
 | 
				
			||||||
 | 
					        settings.PASSWORD_HASHERS.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.assertIs(User(password='some-gibbberish').has_usable_password(), True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_normalize_username(self):
 | 
					    def test_normalize_username(self):
 | 
				
			||||||
        self.assertEqual(IntegerUsernameUser().normalize_username(123), 123)
 | 
					        self.assertEqual(IntegerUsernameUser().normalize_username(123), 123)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user