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.SHA1PasswordHasher', | ||||
|     'django.contrib.auth.hashers.MD5PasswordHasher', | ||||
|     'django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher', | ||||
|     'django.contrib.auth.hashers.UnsaltedMD5PasswordHasher', | ||||
|     'django.contrib.auth.hashers.CryptPasswordHasher', | ||||
| ) | ||||
|   | ||||
| @@ -132,9 +132,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) | ||||
| @@ -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): | ||||
|     """ | ||||
|     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" | ||||
|  | ||||
| @@ -370,6 +409,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') | ||||
|   | ||||
		Reference in New Issue
	
	Block a user