diff --git a/django/contrib/auth/hashers.py b/django/contrib/auth/hashers.py index e53e4da193..ccbac336dc 100644 --- a/django/contrib/auth/hashers.py +++ b/django/contrib/auth/hashers.py @@ -209,6 +209,12 @@ class BasePasswordHasher: """Check if the given password is correct.""" raise NotImplementedError('subclasses of BasePasswordHasher must provide a verify() method') + def _check_encode_args(self, password, salt): + if password is None: + raise TypeError('password must be provided.') + if not salt or '$' in salt: + raise ValueError('salt must be provided and cannot contain $.') + def encode(self, password, salt): """ Create an encoded database value. @@ -269,8 +275,7 @@ class PBKDF2PasswordHasher(BasePasswordHasher): digest = hashlib.sha256 def encode(self, password, salt, iterations=None): - assert password is not None - assert salt and '$' not in salt + self._check_encode_args(password, salt) iterations = iterations or self.iterations hash = pbkdf2(password, salt, iterations, digest=self.digest) hash = base64.b64encode(hash).decode('ascii').strip() @@ -519,8 +524,7 @@ class SHA1PasswordHasher(BasePasswordHasher): algorithm = "sha1" def encode(self, password, salt): - assert password is not None - assert salt and '$' not in salt + self._check_encode_args(password, salt) hash = hashlib.sha1((salt + password).encode()).hexdigest() return "%s$%s$%s" % (self.algorithm, salt, hash) @@ -561,8 +565,7 @@ class MD5PasswordHasher(BasePasswordHasher): algorithm = "md5" def encode(self, password, salt): - assert password is not None - assert salt and '$' not in salt + self._check_encode_args(password, salt) hash = hashlib.md5((salt + password).encode()).hexdigest() return "%s$%s$%s" % (self.algorithm, salt, hash) diff --git a/tests/auth_tests/test_hashers.py b/tests/auth_tests/test_hashers.py index 411bcea8e9..46ac4062f3 100644 --- a/tests/auth_tests/test_hashers.py +++ b/tests/auth_tests/test_hashers.py @@ -4,8 +4,9 @@ from django.conf.global_settings import PASSWORD_HASHERS from django.contrib.auth.hashers import ( UNUSABLE_PASSWORD_PREFIX, UNUSABLE_PASSWORD_SUFFIX_LENGTH, BasePasswordHasher, BCryptPasswordHasher, BCryptSHA256PasswordHasher, - PBKDF2PasswordHasher, PBKDF2SHA1PasswordHasher, check_password, get_hasher, - identify_hasher, is_password_usable, make_password, + MD5PasswordHasher, PBKDF2PasswordHasher, PBKDF2SHA1PasswordHasher, + SHA1PasswordHasher, check_password, get_hasher, identify_hasher, + is_password_usable, make_password, ) from django.test import SimpleTestCase from django.test.utils import override_settings @@ -474,6 +475,35 @@ class TestUtilsHashPass(SimpleTestCase): check_password('wrong_password', encoded) self.assertEqual(hasher.harden_runtime.call_count, 1) + def test_encode_invalid_salt(self): + hasher_classes = [ + MD5PasswordHasher, + PBKDF2PasswordHasher, + PBKDF2SHA1PasswordHasher, + SHA1PasswordHasher, + ] + msg = 'salt must be provided and cannot contain $.' + for hasher_class in hasher_classes: + hasher = hasher_class() + for salt in [None, '', 'sea$salt']: + with self.subTest(hasher_class.__name__, salt=salt): + with self.assertRaisesMessage(ValueError, msg): + hasher.encode('password', salt) + + def test_encode_password_required(self): + hasher_classes = [ + MD5PasswordHasher, + PBKDF2PasswordHasher, + PBKDF2SHA1PasswordHasher, + SHA1PasswordHasher, + ] + msg = 'password must be provided.' + for hasher_class in hasher_classes: + hasher = hasher_class() + with self.subTest(hasher_class.__name__): + with self.assertRaisesMessage(TypeError, msg): + hasher.encode(None, 'seasalt') + class BasePasswordHasherTests(SimpleTestCase): not_implemented_msg = 'subclasses of BasePasswordHasher must provide %s() method'