Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

[1.4.x] Fixed #18144 -- Restored compatibility with SHA1 hashes with …

…empty salt.

Thanks dahool for the report and initial version of the patch.

Backport of 633d8de from master.
  • Loading branch information...
commit 97a67b26f3debc40c73f835dd17cbef98fe5d8c6 1 parent 52bac4e
Aymeric Augustin aaugustin authored
1  django/conf/global_settings.py
View
@@ -516,6 +516,7 @@
'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',
)
52 django/contrib/auth/hashers.py
View
@@ -35,9 +35,14 @@ def check_password(password, encoded, setter=None, preferred='default'):
password = smart_str(password)
encoded = smart_str(encoded)
+ # 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$$'))):
hasher = get_hasher('unsalted_md5')
+ # Ancient versions of Django accepted SHA1 passwords with an empty salt.
+ elif len(encoded) == 46 and encoded.startswith('sha1$$'):
+ hasher = get_hasher('unsalted_sha1')
else:
algorithm = encoded.split('$', 1)[0]
hasher = get_hasher(algorithm)
@@ -330,14 +335,48 @@ def safe_summary(self, encoded):
])
-class UnsaltedMD5PasswordHasher(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.
"""
- I am an incredibly insecure algorithm you should *never* use;
- stores unsalted MD5 hashes without the algorithm prefix.
+ algorithm = "unsalted_sha1"
+
+ def salt(self):
+ return ''
- 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.
+ def encode(self, password, salt):
+ assert salt == ''
+ hash = hashlib.sha1(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):
+ """
+ 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
+ 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"
@@ -345,6 +384,7 @@ def salt(self):
return ''
def encode(self, password, salt):
+ assert salt == ''
return hashlib.md5(password).hexdigest()
def verify(self, password, encoded):
24 django/contrib/auth/tests/hashers.py
View
@@ -1,5 +1,5 @@
from django.conf.global_settings import PASSWORD_HASHERS as default_hashers
-from django.contrib.auth.hashers import (is_password_usable,
+from django.contrib.auth.hashers import (is_password_usable,
check_password, make_password, PBKDF2PasswordHasher, load_hashers,
PBKDF2SHA1PasswordHasher, get_hasher, UNUSABLE_PASSWORD)
from django.utils import unittest
@@ -31,7 +31,7 @@ def test_simple(self):
def test_pkbdf2(self):
encoded = make_password('letmein', 'seasalt', 'pbkdf2_sha256')
- self.assertEqual(encoded,
+ self.assertEqual(encoded,
'pbkdf2_sha256$10000$seasalt$FQCNpiZpTb0zub+HBsH6TOwyRxJ19FwvjbweatNmK/Y=')
self.assertTrue(is_password_usable(encoded))
self.assertTrue(check_password(u'letmein', encoded))
@@ -39,7 +39,7 @@ def test_pkbdf2(self):
def test_sha1(self):
encoded = make_password('letmein', 'seasalt', 'sha1')
- self.assertEqual(encoded,
+ self.assertEqual(encoded,
'sha1$seasalt$fec3530984afba6bade3347b7140d1a7da7da8c7')
self.assertTrue(is_password_usable(encoded))
self.assertTrue(check_password(u'letmein', encoded))
@@ -47,14 +47,14 @@ def test_sha1(self):
def test_md5(self):
encoded = make_password('letmein', 'seasalt', 'md5')
- self.assertEqual(encoded,
+ self.assertEqual(encoded,
'md5$seasalt$f5531bef9f3687d0ccf0f617f0e25573')
self.assertTrue(is_password_usable(encoded))
self.assertTrue(check_password(u'letmein', encoded))
self.assertFalse(check_password('letmeinz', encoded))
def test_unsalted_md5(self):
- encoded = make_password('letmein', 'seasalt', 'unsalted_md5')
+ encoded = make_password('letmein', '', 'unsalted_md5')
self.assertEqual(encoded, '0d107d09f5bbe40cade3de5c71e9e9b7')
self.assertTrue(is_password_usable(encoded))
self.assertTrue(check_password(u'letmein', encoded))
@@ -65,6 +65,16 @@ def test_unsalted_md5(self):
self.assertTrue(check_password(u'letmein', alt_encoded))
self.assertFalse(check_password('letmeinz', alt_encoded))
+ def test_unsalted_sha1(self):
+ encoded = make_password('letmein', '', 'unsalted_sha1')
+ self.assertEqual(encoded, 'sha1$$b7a875fc1ea228b9061041b7cec4bd3c52ab3ce3')
+ self.assertTrue(is_password_usable(encoded))
+ self.assertTrue(check_password('letmein', encoded))
+ self.assertFalse(check_password('letmeinz', encoded))
+ # Raw SHA1 isn't acceptable
+ alt_encoded = encoded[6:]
+ self.assertRaises(ValueError, check_password, 'letmein', alt_encoded)
+
@skipUnless(crypt, "no crypt module to generate password.")
def test_crypt(self):
encoded = make_password('letmein', 'ab', 'crypt')
@@ -98,14 +108,14 @@ def doit():
def test_low_level_pkbdf2(self):
hasher = PBKDF2PasswordHasher()
encoded = hasher.encode('letmein', 'seasalt')
- self.assertEqual(encoded,
+ self.assertEqual(encoded,
'pbkdf2_sha256$10000$seasalt$FQCNpiZpTb0zub+HBsH6TOwyRxJ19FwvjbweatNmK/Y=')
self.assertTrue(hasher.verify('letmein', encoded))
def test_low_level_pbkdf2_sha1(self):
hasher = PBKDF2SHA1PasswordHasher()
encoded = hasher.encode('letmein', 'seasalt')
- self.assertEqual(encoded,
+ self.assertEqual(encoded,
'pbkdf2_sha1$10000$seasalt$91JiNKgwADC8j2j86Ije/cc4vfQ=')
self.assertTrue(hasher.verify('letmein', encoded))
Please sign in to comment.
Something went wrong with that request. Please try again.