Fixed #28718 -- Allowed user to request a password reset if their password doesn't use an enabled hasher.

Regression in aeb1389442.
Reverted changes to is_password_usable() from
703c266682 and documentation changes from
92f48680db.
This commit is contained in:
Tim Graham 2018-03-20 17:19:27 -04:00
parent d97cce3409
commit a4f0e9aec7
7 changed files with 50 additions and 19 deletions

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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:

View File

@ -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()

View File

@ -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)