Fixed #18144 -- Restored compatibility with SHA1 hashes with empty salt.
Thanks dahool for the report and initial version of the patch.
This commit is contained in:
parent
509798ae06
commit
f1255a3c09
|
@ -510,6 +510,7 @@ PASSWORD_HASHERS = (
|
|||
'django.contrib.auth.hashers.BCryptPasswordHasher',
|
||||
'django.contrib.auth.hashers.SHA1PasswordHasher',
|
||||
'django.contrib.auth.hashers.MD5PasswordHasher',
|
||||
'django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher',
|
||||
'django.contrib.auth.hashers.UnsaltedMD5PasswordHasher',
|
||||
'django.contrib.auth.hashers.CryptPasswordHasher',
|
||||
)
|
||||
|
|
|
@ -127,9 +127,14 @@ def identify_hasher(encoded):
|
|||
get_hasher() to return hasher. Raises ValueError if
|
||||
algorithm cannot be identified, or if hasher is not loaded.
|
||||
"""
|
||||
# Ancient versions of Django created plain MD5 passwords and accepted
|
||||
# MD5 passwords with an empty salt.
|
||||
if ((len(encoded) == 32 and '$' not in encoded) or
|
||||
(len(encoded) == 37 and encoded.startswith('md5$$'))):
|
||||
algorithm = 'unsalted_md5'
|
||||
# Ancient versions of Django accepted SHA1 passwords with an empty salt.
|
||||
elif len(encoded) == 46 and encoded.startswith('sha1$$'):
|
||||
algorithm = 'unsalted_sha1'
|
||||
else:
|
||||
algorithm = encoded.split('$', 1)[0]
|
||||
return get_hasher(algorithm)
|
||||
|
@ -350,14 +355,48 @@ class MD5PasswordHasher(BasePasswordHasher):
|
|||
])
|
||||
|
||||
|
||||
class UnsaltedSHA1PasswordHasher(BasePasswordHasher):
|
||||
"""
|
||||
Very insecure algorithm that you should *never* use; stores SHA1 hashes
|
||||
with an empty salt.
|
||||
|
||||
This class is implemented because Django used to accept such password
|
||||
hashes. Some older Django installs still have these values lingering
|
||||
around so we need to handle and upgrade them properly.
|
||||
"""
|
||||
algorithm = "unsalted_sha1"
|
||||
|
||||
def salt(self):
|
||||
return ''
|
||||
|
||||
def encode(self, password, salt):
|
||||
assert salt == ''
|
||||
hash = hashlib.sha1(force_bytes(password)).hexdigest()
|
||||
return 'sha1$$%s' % hash
|
||||
|
||||
def verify(self, password, encoded):
|
||||
encoded_2 = self.encode(password, '')
|
||||
return constant_time_compare(encoded, encoded_2)
|
||||
|
||||
def safe_summary(self, encoded):
|
||||
assert encoded.startswith('sha1$$')
|
||||
hash = encoded[6:]
|
||||
return SortedDict([
|
||||
(_('algorithm'), self.algorithm),
|
||||
(_('hash'), mask_hash(hash)),
|
||||
])
|
||||
|
||||
|
||||
class UnsaltedMD5PasswordHasher(BasePasswordHasher):
|
||||
"""
|
||||
I am an incredibly insecure algorithm you should *never* use;
|
||||
stores unsalted MD5 hashes without the algorithm prefix.
|
||||
Incredibly insecure algorithm that you should *never* use; stores unsalted
|
||||
MD5 hashes without the algorithm prefix, also accepts MD5 hashes with an
|
||||
empty salt.
|
||||
|
||||
This class is implemented because Django used to store passwords
|
||||
this way. Some older Django installs still have these values
|
||||
lingering around so we need to handle and upgrade them properly.
|
||||
This class is implemented because Django used to store passwords this way
|
||||
and to accept such password hashes. Some older Django installs still have
|
||||
these values lingering around so we need to handle and upgrade them
|
||||
properly.
|
||||
"""
|
||||
algorithm = "unsalted_md5"
|
||||
|
||||
|
@ -365,6 +404,7 @@ class UnsaltedMD5PasswordHasher(BasePasswordHasher):
|
|||
return ''
|
||||
|
||||
def encode(self, password, salt):
|
||||
assert salt == ''
|
||||
return hashlib.md5(force_bytes(password)).hexdigest()
|
||||
|
||||
def verify(self, password, encoded):
|
||||
|
|
|
@ -60,7 +60,7 @@ class TestUtilsHashPass(unittest.TestCase):
|
|||
self.assertEqual(identify_hasher(encoded).algorithm, "md5")
|
||||
|
||||
def test_unsalted_md5(self):
|
||||
encoded = make_password('lètmein', 'seasalt', 'unsalted_md5')
|
||||
encoded = make_password('lètmein', '', 'unsalted_md5')
|
||||
self.assertEqual(encoded, '88a434c88cca4e900f7874cd98123f43')
|
||||
self.assertTrue(is_password_usable(encoded))
|
||||
self.assertTrue(check_password('lètmein', encoded))
|
||||
|
@ -72,6 +72,17 @@ class TestUtilsHashPass(unittest.TestCase):
|
|||
self.assertTrue(check_password('lètmein', alt_encoded))
|
||||
self.assertFalse(check_password('lètmeinz', alt_encoded))
|
||||
|
||||
def test_unsalted_sha1(self):
|
||||
encoded = make_password('lètmein', '', 'unsalted_sha1')
|
||||
self.assertEqual(encoded, 'sha1$$6d138ca3ae545631b3abd71a4f076ce759c5700b')
|
||||
self.assertTrue(is_password_usable(encoded))
|
||||
self.assertTrue(check_password('lètmein', encoded))
|
||||
self.assertFalse(check_password('lètmeinz', encoded))
|
||||
self.assertEqual(identify_hasher(encoded).algorithm, "unsalted_sha1")
|
||||
# Raw SHA1 isn't acceptable
|
||||
alt_encoded = encoded[6:]
|
||||
self.assertFalse(check_password('lètmein', alt_encoded))
|
||||
|
||||
@skipUnless(crypt, "no crypt module to generate password.")
|
||||
def test_crypt(self):
|
||||
encoded = make_password('lètmei', 'ab', 'crypt')
|
||||
|
|
Loading…
Reference in New Issue