mirror of
				https://github.com/django/django.git
				synced 2025-10-30 17:16:10 +00:00 
			
		
		
		
	[1.5.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.
This commit is contained in:
		| @@ -518,6 +518,7 @@ PASSWORD_HASHERS = ( | |||||||
|     'django.contrib.auth.hashers.BCryptPasswordHasher', |     'django.contrib.auth.hashers.BCryptPasswordHasher', | ||||||
|     'django.contrib.auth.hashers.SHA1PasswordHasher', |     'django.contrib.auth.hashers.SHA1PasswordHasher', | ||||||
|     'django.contrib.auth.hashers.MD5PasswordHasher', |     'django.contrib.auth.hashers.MD5PasswordHasher', | ||||||
|  |     'django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher', | ||||||
|     'django.contrib.auth.hashers.UnsaltedMD5PasswordHasher', |     'django.contrib.auth.hashers.UnsaltedMD5PasswordHasher', | ||||||
|     'django.contrib.auth.hashers.CryptPasswordHasher', |     'django.contrib.auth.hashers.CryptPasswordHasher', | ||||||
| ) | ) | ||||||
|   | |||||||
| @@ -132,9 +132,14 @@ def identify_hasher(encoded): | |||||||
|     get_hasher() to return hasher. Raises ValueError if |     get_hasher() to return hasher. Raises ValueError if | ||||||
|     algorithm cannot be identified, or if hasher is not loaded. |     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 |     if ((len(encoded) == 32 and '$' not in encoded) or | ||||||
|             (len(encoded) == 37 and encoded.startswith('md5$$'))): |             (len(encoded) == 37 and encoded.startswith('md5$$'))): | ||||||
|         algorithm = 'unsalted_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: |     else: | ||||||
|         algorithm = encoded.split('$', 1)[0] |         algorithm = encoded.split('$', 1)[0] | ||||||
|     return get_hasher(algorithm) |     return get_hasher(algorithm) | ||||||
| @@ -355,14 +360,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): | class UnsaltedMD5PasswordHasher(BasePasswordHasher): | ||||||
|     """ |     """ | ||||||
|     I am an incredibly insecure algorithm you should *never* use; |     Incredibly insecure algorithm that you should *never* use; stores unsalted | ||||||
|     stores unsalted MD5 hashes without the algorithm prefix. |     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 class is implemented because Django used to store passwords this way | ||||||
|     this way. Some older Django installs still have these values |     and to accept such password hashes. Some older Django installs still have | ||||||
|     lingering around so we need to handle and upgrade them properly. |     these values lingering around so we need to handle and upgrade them | ||||||
|  |     properly. | ||||||
|     """ |     """ | ||||||
|     algorithm = "unsalted_md5" |     algorithm = "unsalted_md5" | ||||||
|  |  | ||||||
| @@ -370,6 +409,7 @@ class UnsaltedMD5PasswordHasher(BasePasswordHasher): | |||||||
|         return '' |         return '' | ||||||
|  |  | ||||||
|     def encode(self, password, salt): |     def encode(self, password, salt): | ||||||
|  |         assert salt == '' | ||||||
|         return hashlib.md5(force_bytes(password)).hexdigest() |         return hashlib.md5(force_bytes(password)).hexdigest() | ||||||
|  |  | ||||||
|     def verify(self, password, encoded): |     def verify(self, password, encoded): | ||||||
|   | |||||||
| @@ -60,7 +60,7 @@ class TestUtilsHashPass(unittest.TestCase): | |||||||
|         self.assertEqual(identify_hasher(encoded).algorithm, "md5") |         self.assertEqual(identify_hasher(encoded).algorithm, "md5") | ||||||
|  |  | ||||||
|     def test_unsalted_md5(self): |     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.assertEqual(encoded, '88a434c88cca4e900f7874cd98123f43') | ||||||
|         self.assertTrue(is_password_usable(encoded)) |         self.assertTrue(is_password_usable(encoded)) | ||||||
|         self.assertTrue(check_password('lètmein', 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.assertTrue(check_password('lètmein', alt_encoded)) | ||||||
|         self.assertFalse(check_password('lètmeinz', 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.") |     @skipUnless(crypt, "no crypt module to generate password.") | ||||||
|     def test_crypt(self): |     def test_crypt(self): | ||||||
|         encoded = make_password('lètmei', 'ab', 'crypt') |         encoded = make_password('lètmei', 'ab', 'crypt') | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user