mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	Refs #33691 -- Removed insecure password hashers per deprecation timeline.
This commit is contained in:
		| @@ -16,7 +16,6 @@ from django.utils.crypto import ( | |||||||
|     get_random_string, |     get_random_string, | ||||||
|     pbkdf2, |     pbkdf2, | ||||||
| ) | ) | ||||||
| from django.utils.deprecation import RemovedInDjango51Warning |  | ||||||
| from django.utils.module_loading import import_string | from django.utils.module_loading import import_string | ||||||
| from django.utils.translation import gettext_noop as _ | from django.utils.translation import gettext_noop as _ | ||||||
|  |  | ||||||
| @@ -641,57 +640,6 @@ class ScryptPasswordHasher(BasePasswordHasher): | |||||||
|         pass |         pass | ||||||
|  |  | ||||||
|  |  | ||||||
| # RemovedInDjango51Warning. |  | ||||||
| class SHA1PasswordHasher(BasePasswordHasher): |  | ||||||
|     """ |  | ||||||
|     The SHA1 password hashing algorithm (not recommended) |  | ||||||
|     """ |  | ||||||
|  |  | ||||||
|     algorithm = "sha1" |  | ||||||
|  |  | ||||||
|     def __init__(self, *args, **kwargs): |  | ||||||
|         warnings.warn( |  | ||||||
|             "django.contrib.auth.hashers.SHA1PasswordHasher is deprecated.", |  | ||||||
|             RemovedInDjango51Warning, |  | ||||||
|             stacklevel=2, |  | ||||||
|         ) |  | ||||||
|         super().__init__(*args, **kwargs) |  | ||||||
|  |  | ||||||
|     def encode(self, password, salt): |  | ||||||
|         self._check_encode_args(password, salt) |  | ||||||
|         hash = hashlib.sha1((salt + password).encode()).hexdigest() |  | ||||||
|         return "%s$%s$%s" % (self.algorithm, salt, hash) |  | ||||||
|  |  | ||||||
|     def decode(self, encoded): |  | ||||||
|         algorithm, salt, hash = encoded.split("$", 2) |  | ||||||
|         assert algorithm == self.algorithm |  | ||||||
|         return { |  | ||||||
|             "algorithm": algorithm, |  | ||||||
|             "hash": hash, |  | ||||||
|             "salt": salt, |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|     def verify(self, password, encoded): |  | ||||||
|         decoded = self.decode(encoded) |  | ||||||
|         encoded_2 = self.encode(password, decoded["salt"]) |  | ||||||
|         return constant_time_compare(encoded, encoded_2) |  | ||||||
|  |  | ||||||
|     def safe_summary(self, encoded): |  | ||||||
|         decoded = self.decode(encoded) |  | ||||||
|         return { |  | ||||||
|             _("algorithm"): decoded["algorithm"], |  | ||||||
|             _("salt"): mask_hash(decoded["salt"], show=2), |  | ||||||
|             _("hash"): mask_hash(decoded["hash"]), |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|     def must_update(self, encoded): |  | ||||||
|         decoded = self.decode(encoded) |  | ||||||
|         return must_update_salt(decoded["salt"], self.salt_entropy) |  | ||||||
|  |  | ||||||
|     def harden_runtime(self, password, encoded): |  | ||||||
|         pass |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class MD5PasswordHasher(BasePasswordHasher): | class MD5PasswordHasher(BasePasswordHasher): | ||||||
|     """ |     """ | ||||||
|     The Salted MD5 password hashing algorithm (not recommended) |     The Salted MD5 password hashing algorithm (not recommended) | ||||||
| @@ -732,111 +680,3 @@ class MD5PasswordHasher(BasePasswordHasher): | |||||||
|  |  | ||||||
|     def harden_runtime(self, password, encoded): |     def harden_runtime(self, password, encoded): | ||||||
|         pass |         pass | ||||||
|  |  | ||||||
|  |  | ||||||
| # RemovedInDjango51Warning. |  | ||||||
| class UnsaltedSHA1PasswordHasher(BasePasswordHasher): |  | ||||||
|     """ |  | ||||||
|     Very insecure algorithm that you should *never* use; store 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 __init__(self, *args, **kwargs): |  | ||||||
|         warnings.warn( |  | ||||||
|             "django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher is deprecated.", |  | ||||||
|             RemovedInDjango51Warning, |  | ||||||
|             stacklevel=2, |  | ||||||
|         ) |  | ||||||
|         super().__init__(*args, **kwargs) |  | ||||||
|  |  | ||||||
|     def salt(self): |  | ||||||
|         return "" |  | ||||||
|  |  | ||||||
|     def encode(self, password, salt): |  | ||||||
|         if salt != "": |  | ||||||
|             raise ValueError("salt must be empty.") |  | ||||||
|         hash = hashlib.sha1(password.encode()).hexdigest() |  | ||||||
|         return "sha1$$%s" % hash |  | ||||||
|  |  | ||||||
|     def decode(self, encoded): |  | ||||||
|         assert encoded.startswith("sha1$$") |  | ||||||
|         return { |  | ||||||
|             "algorithm": self.algorithm, |  | ||||||
|             "hash": encoded[6:], |  | ||||||
|             "salt": None, |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|     def verify(self, password, encoded): |  | ||||||
|         encoded_2 = self.encode(password, "") |  | ||||||
|         return constant_time_compare(encoded, encoded_2) |  | ||||||
|  |  | ||||||
|     def safe_summary(self, encoded): |  | ||||||
|         decoded = self.decode(encoded) |  | ||||||
|         return { |  | ||||||
|             _("algorithm"): decoded["algorithm"], |  | ||||||
|             _("hash"): mask_hash(decoded["hash"]), |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|     def harden_runtime(self, password, encoded): |  | ||||||
|         pass |  | ||||||
|  |  | ||||||
|  |  | ||||||
| # RemovedInDjango51Warning. |  | ||||||
| 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" |  | ||||||
|  |  | ||||||
|     def __init__(self, *args, **kwargs): |  | ||||||
|         warnings.warn( |  | ||||||
|             "django.contrib.auth.hashers.UnsaltedMD5PasswordHasher is deprecated.", |  | ||||||
|             RemovedInDjango51Warning, |  | ||||||
|             stacklevel=2, |  | ||||||
|         ) |  | ||||||
|         super().__init__(*args, **kwargs) |  | ||||||
|  |  | ||||||
|     def salt(self): |  | ||||||
|         return "" |  | ||||||
|  |  | ||||||
|     def encode(self, password, salt): |  | ||||||
|         if salt != "": |  | ||||||
|             raise ValueError("salt must be empty.") |  | ||||||
|         return hashlib.md5(password.encode()).hexdigest() |  | ||||||
|  |  | ||||||
|     def decode(self, encoded): |  | ||||||
|         return { |  | ||||||
|             "algorithm": self.algorithm, |  | ||||||
|             "hash": encoded, |  | ||||||
|             "salt": None, |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|     def verify(self, password, encoded): |  | ||||||
|         if len(encoded) == 37: |  | ||||||
|             encoded = encoded.removeprefix("md5$$") |  | ||||||
|         encoded_2 = self.encode(password, "") |  | ||||||
|         return constant_time_compare(encoded, encoded_2) |  | ||||||
|  |  | ||||||
|     def safe_summary(self, encoded): |  | ||||||
|         decoded = self.decode(encoded) |  | ||||||
|         return { |  | ||||||
|             _("algorithm"): decoded["algorithm"], |  | ||||||
|             _("hash"): mask_hash(decoded["hash"], show=3), |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|     def harden_runtime(self, password, encoded): |  | ||||||
|         pass |  | ||||||
|   | |||||||
| @@ -256,3 +256,7 @@ to remove usage of these features. | |||||||
| * The model's ``Meta.index_together`` option is removed. | * The model's ``Meta.index_together`` option is removed. | ||||||
|  |  | ||||||
| * The ``length_is`` template filter is removed. | * The ``length_is`` template filter is removed. | ||||||
|  |  | ||||||
|  | * The ``django.contrib.auth.hashers.SHA1PasswordHasher``, | ||||||
|  |   ``django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher``, and | ||||||
|  |   ``django.contrib.auth.hashers.UnsaltedMD5PasswordHasher`` are removed. | ||||||
|   | |||||||
| @@ -18,9 +18,8 @@ from django.contrib.auth.hashers import ( | |||||||
|     is_password_usable, |     is_password_usable, | ||||||
|     make_password, |     make_password, | ||||||
| ) | ) | ||||||
| from django.test import SimpleTestCase, ignore_warnings | from django.test import SimpleTestCase | ||||||
| from django.test.utils import override_settings | from django.test.utils import override_settings | ||||||
| from django.utils.deprecation import RemovedInDjango51Warning |  | ||||||
|  |  | ||||||
| try: | try: | ||||||
|     import bcrypt |     import bcrypt | ||||||
| @@ -103,40 +102,6 @@ class TestUtilsHashPass(SimpleTestCase): | |||||||
|         self.assertIs(hasher.must_update(encoded_weak_salt), True) |         self.assertIs(hasher.must_update(encoded_weak_salt), True) | ||||||
|         self.assertIs(hasher.must_update(encoded_strong_salt), False) |         self.assertIs(hasher.must_update(encoded_strong_salt), False) | ||||||
|  |  | ||||||
|     @ignore_warnings(category=RemovedInDjango51Warning) |  | ||||||
|     @override_settings( |  | ||||||
|         PASSWORD_HASHERS=["django.contrib.auth.hashers.SHA1PasswordHasher"] |  | ||||||
|     ) |  | ||||||
|     def test_sha1(self): |  | ||||||
|         encoded = make_password("lètmein", "seasalt", "sha1") |  | ||||||
|         self.assertEqual( |  | ||||||
|             encoded, "sha1$seasalt$cff36ea83f5706ce9aa7454e63e431fc726b2dc8" |  | ||||||
|         ) |  | ||||||
|         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, "sha1") |  | ||||||
|         # Blank passwords |  | ||||||
|         blank_encoded = make_password("", "seasalt", "sha1") |  | ||||||
|         self.assertTrue(blank_encoded.startswith("sha1$")) |  | ||||||
|         self.assertTrue(is_password_usable(blank_encoded)) |  | ||||||
|         self.assertTrue(check_password("", blank_encoded)) |  | ||||||
|         self.assertFalse(check_password(" ", blank_encoded)) |  | ||||||
|         # Salt entropy check. |  | ||||||
|         hasher = get_hasher("sha1") |  | ||||||
|         encoded_weak_salt = make_password("lètmein", "iodizedsalt", "sha1") |  | ||||||
|         encoded_strong_salt = make_password("lètmein", hasher.salt(), "sha1") |  | ||||||
|         self.assertIs(hasher.must_update(encoded_weak_salt), True) |  | ||||||
|         self.assertIs(hasher.must_update(encoded_strong_salt), False) |  | ||||||
|  |  | ||||||
|     @override_settings( |  | ||||||
|         PASSWORD_HASHERS=["django.contrib.auth.hashers.SHA1PasswordHasher"] |  | ||||||
|     ) |  | ||||||
|     def test_sha1_deprecation_warning(self): |  | ||||||
|         msg = "django.contrib.auth.hashers.SHA1PasswordHasher is deprecated." |  | ||||||
|         with self.assertRaisesMessage(RemovedInDjango51Warning, msg): |  | ||||||
|             get_hasher("sha1") |  | ||||||
|  |  | ||||||
|     @override_settings( |     @override_settings( | ||||||
|         PASSWORD_HASHERS=["django.contrib.auth.hashers.MD5PasswordHasher"] |         PASSWORD_HASHERS=["django.contrib.auth.hashers.MD5PasswordHasher"] | ||||||
|     ) |     ) | ||||||
| @@ -160,85 +125,6 @@ class TestUtilsHashPass(SimpleTestCase): | |||||||
|         self.assertIs(hasher.must_update(encoded_weak_salt), True) |         self.assertIs(hasher.must_update(encoded_weak_salt), True) | ||||||
|         self.assertIs(hasher.must_update(encoded_strong_salt), False) |         self.assertIs(hasher.must_update(encoded_strong_salt), False) | ||||||
|  |  | ||||||
|     @ignore_warnings(category=RemovedInDjango51Warning) |  | ||||||
|     @override_settings( |  | ||||||
|         PASSWORD_HASHERS=["django.contrib.auth.hashers.UnsaltedMD5PasswordHasher"] |  | ||||||
|     ) |  | ||||||
|     def test_unsalted_md5(self): |  | ||||||
|         encoded = make_password("lètmein", "", "unsalted_md5") |  | ||||||
|         self.assertEqual(encoded, "88a434c88cca4e900f7874cd98123f43") |  | ||||||
|         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_md5") |  | ||||||
|         # Alternate unsalted syntax |  | ||||||
|         alt_encoded = "md5$$%s" % encoded |  | ||||||
|         self.assertTrue(is_password_usable(alt_encoded)) |  | ||||||
|         self.assertTrue(check_password("lètmein", alt_encoded)) |  | ||||||
|         self.assertFalse(check_password("lètmeinz", alt_encoded)) |  | ||||||
|         # Blank passwords |  | ||||||
|         blank_encoded = make_password("", "", "unsalted_md5") |  | ||||||
|         self.assertTrue(is_password_usable(blank_encoded)) |  | ||||||
|         self.assertTrue(check_password("", blank_encoded)) |  | ||||||
|         self.assertFalse(check_password(" ", blank_encoded)) |  | ||||||
|  |  | ||||||
|     @ignore_warnings(category=RemovedInDjango51Warning) |  | ||||||
|     @override_settings( |  | ||||||
|         PASSWORD_HASHERS=["django.contrib.auth.hashers.UnsaltedMD5PasswordHasher"] |  | ||||||
|     ) |  | ||||||
|     def test_unsalted_md5_encode_invalid_salt(self): |  | ||||||
|         hasher = get_hasher("unsalted_md5") |  | ||||||
|         msg = "salt must be empty." |  | ||||||
|         with self.assertRaisesMessage(ValueError, msg): |  | ||||||
|             hasher.encode("password", salt="salt") |  | ||||||
|  |  | ||||||
|     @override_settings( |  | ||||||
|         PASSWORD_HASHERS=["django.contrib.auth.hashers.UnsaltedMD5PasswordHasher"] |  | ||||||
|     ) |  | ||||||
|     def test_unsalted_md5_deprecation_warning(self): |  | ||||||
|         msg = "django.contrib.auth.hashers.UnsaltedMD5PasswordHasher is deprecated." |  | ||||||
|         with self.assertRaisesMessage(RemovedInDjango51Warning, msg): |  | ||||||
|             get_hasher("unsalted_md5") |  | ||||||
|  |  | ||||||
|     @ignore_warnings(category=RemovedInDjango51Warning) |  | ||||||
|     @override_settings( |  | ||||||
|         PASSWORD_HASHERS=["django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher"] |  | ||||||
|     ) |  | ||||||
|     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)) |  | ||||||
|         # Blank passwords |  | ||||||
|         blank_encoded = make_password("", "", "unsalted_sha1") |  | ||||||
|         self.assertTrue(blank_encoded.startswith("sha1$")) |  | ||||||
|         self.assertTrue(is_password_usable(blank_encoded)) |  | ||||||
|         self.assertTrue(check_password("", blank_encoded)) |  | ||||||
|         self.assertFalse(check_password(" ", blank_encoded)) |  | ||||||
|  |  | ||||||
|     @ignore_warnings(category=RemovedInDjango51Warning) |  | ||||||
|     @override_settings( |  | ||||||
|         PASSWORD_HASHERS=["django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher"] |  | ||||||
|     ) |  | ||||||
|     def test_unsalted_sha1_encode_invalid_salt(self): |  | ||||||
|         hasher = get_hasher("unsalted_sha1") |  | ||||||
|         msg = "salt must be empty." |  | ||||||
|         with self.assertRaisesMessage(ValueError, msg): |  | ||||||
|             hasher.encode("password", salt="salt") |  | ||||||
|  |  | ||||||
|     @override_settings( |  | ||||||
|         PASSWORD_HASHERS=["django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher"] |  | ||||||
|     ) |  | ||||||
|     def test_unsalted_sha1_deprecation_warning(self): |  | ||||||
|         msg = "django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher is deprecated." |  | ||||||
|         with self.assertRaisesMessage(RemovedInDjango51Warning, msg): |  | ||||||
|             get_hasher("unsalted_sha1") |  | ||||||
|  |  | ||||||
|     @skipUnless(bcrypt, "bcrypt not installed") |     @skipUnless(bcrypt, "bcrypt not installed") | ||||||
|     def test_bcrypt_sha256(self): |     def test_bcrypt_sha256(self): | ||||||
|         encoded = make_password("lètmein", hasher="bcrypt_sha256") |         encoded = make_password("lètmein", hasher="bcrypt_sha256") | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user