Browse Source

Refs #33691 -- Deprecated insecure password hashers.

SHA1PasswordHasher, UnsaltedSHA1PasswordHasher, and UnsaltedMD5PasswordHasher
are now deprecated.
Claude Paroz 2 years ago
parent
commit
3b79dab19a

+ 28 - 1
django/contrib/auth/hashers.py

@@ -17,7 +17,7 @@ from django.utils.crypto import (
     md5,
     pbkdf2,
 )
-from django.utils.deprecation import RemovedInDjango50Warning
+from django.utils.deprecation import RemovedInDjango50Warning, RemovedInDjango51Warning
 from django.utils.module_loading import import_string
 from django.utils.translation import gettext_noop as _
 
@@ -624,6 +624,7 @@ class ScryptPasswordHasher(BasePasswordHasher):
         pass
 
 
+# RemovedInDjango51Warning.
 class SHA1PasswordHasher(BasePasswordHasher):
     """
     The SHA1 password hashing algorithm (not recommended)
@@ -631,6 +632,14 @@ class SHA1PasswordHasher(BasePasswordHasher):
 
     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()
@@ -708,6 +717,7 @@ class MD5PasswordHasher(BasePasswordHasher):
         pass
 
 
+# RemovedInDjango51Warning.
 class UnsaltedSHA1PasswordHasher(BasePasswordHasher):
     """
     Very insecure algorithm that you should *never* use; store SHA1 hashes
@@ -720,6 +730,14 @@ class UnsaltedSHA1PasswordHasher(BasePasswordHasher):
 
     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 ""
 
@@ -752,6 +770,7 @@ class UnsaltedSHA1PasswordHasher(BasePasswordHasher):
         pass
 
 
+# RemovedInDjango51Warning.
 class UnsaltedMD5PasswordHasher(BasePasswordHasher):
     """
     Incredibly insecure algorithm that you should *never* use; stores unsalted
@@ -766,6 +785,14 @@ class UnsaltedMD5PasswordHasher(BasePasswordHasher):
 
     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 ""
 

+ 4 - 0
docs/internals/deprecation.txt

@@ -24,6 +24,10 @@ details on these changes.
 
 * The ``length_is`` template filter will be removed.
 
+* The ``django.contrib.auth.hashers.SHA1PasswordHasher``,
+  ``django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher``, and
+  ``django.contrib.auth.hashers.UnsaltedMD5PasswordHasher`` will be removed.
+
 .. _deprecation-removed-in-5.0:
 
 5.0

+ 4 - 0
docs/releases/4.2.txt

@@ -332,3 +332,7 @@ Miscellaneous
 
     {% if value|length_is:4 %}…{% endif %}
     {{ value|length_is:4 }}
+
+* ``django.contrib.auth.hashers.SHA1PasswordHasher``,
+  ``django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher``, and
+  ``django.contrib.auth.hashers.UnsaltedMD5PasswordHasher`` are deprecated.

+ 19 - 26
docs/topics/auth/passwords.txt

@@ -329,12 +329,12 @@ to mitigate this by :ref:`upgrading older password hashes
 Password upgrading without requiring a login
 --------------------------------------------
 
-If you have an existing database with an older, weak hash such as MD5 or SHA1,
-you might want to upgrade those hashes yourself instead of waiting for the
-upgrade to happen when a user logs in (which may never happen if a user doesn't
-return to your site). In this case, you can use a "wrapped" password hasher.
+If you have an existing database with an older, weak hash such as MD5, you
+might want to upgrade those hashes yourself instead of waiting for the upgrade
+to happen when a user logs in (which may never happen if a user doesn't return
+to your site). In this case, you can use a "wrapped" password hasher.
 
-For this example, we'll migrate a collection of SHA1 hashes to use
+For this example, we'll migrate a collection of MD5 hashes to use
 PBKDF2(SHA1(password)) and add the corresponding password hasher for checking
 if a user entered the correct password on login. We assume we're using the
 built-in ``User`` model and that our project has an ``accounts`` app. You can
@@ -346,37 +346,37 @@ First, we'll add the custom hasher:
     :caption: ``accounts/hashers.py``
 
     from django.contrib.auth.hashers import (
-        PBKDF2PasswordHasher, SHA1PasswordHasher,
+        PBKDF2PasswordHasher, MD5PasswordHasher,
     )
 
 
-    class PBKDF2WrappedSHA1PasswordHasher(PBKDF2PasswordHasher):
-        algorithm = 'pbkdf2_wrapped_sha1'
+    class PBKDF2WrappedMD5PasswordHasher(PBKDF2PasswordHasher):
+        algorithm = 'pbkdf2_wrapped_md5'
 
-        def encode_sha1_hash(self, sha1_hash, salt, iterations=None):
-            return super().encode(sha1_hash, salt, iterations)
+        def encode_md5_hash(self, md5_hash, salt, iterations=None):
+            return super().encode(md5_hash, salt, iterations)
 
         def encode(self, password, salt, iterations=None):
-            _, _, sha1_hash = SHA1PasswordHasher().encode(password, salt).split('$', 2)
-            return self.encode_sha1_hash(sha1_hash, salt, iterations)
+            _, _, md5_hash = MD5PasswordHasher().encode(password, salt).split('$', 2)
+            return self.encode_md5_hash(md5_hash, salt, iterations)
 
 The data migration might look something like:
 
 .. code-block:: python
-    :caption: ``accounts/migrations/0002_migrate_sha1_passwords.py``
+    :caption: ``accounts/migrations/0002_migrate_md5_passwords.py``
 
     from django.db import migrations
 
-    from ..hashers import PBKDF2WrappedSHA1PasswordHasher
+    from ..hashers import PBKDF2WrappedMD5PasswordHasher
 
 
     def forwards_func(apps, schema_editor):
         User = apps.get_model('auth', 'User')
-        users = User.objects.filter(password__startswith='sha1$')
-        hasher = PBKDF2WrappedSHA1PasswordHasher()
+        users = User.objects.filter(password__startswith='md5$')
+        hasher = PBKDF2WrappedMD5PasswordHasher()
         for user in users:
-            algorithm, salt, sha1_hash = user.password.split('$', 2)
-            user.password = hasher.encode_sha1_hash(sha1_hash, salt)
+            algorithm, salt, md5_hash = user.password.split('$', 2)
+            user.password = hasher.encode_md5_hash(md5_hash, salt)
             user.save(update_fields=['password'])
 
 
@@ -402,12 +402,11 @@ Finally, we'll add a :setting:`PASSWORD_HASHERS` setting:
 
     PASSWORD_HASHERS = [
         'django.contrib.auth.hashers.PBKDF2PasswordHasher',
-        'accounts.hashers.PBKDF2WrappedSHA1PasswordHasher',
+        'accounts.hashers.PBKDF2WrappedMD5PasswordHasher',
     ]
 
 Include any other hashers that your site uses in this list.
 
-.. _sha1: https://en.wikipedia.org/wiki/SHA1
 .. _pbkdf2: https://en.wikipedia.org/wiki/PBKDF2
 .. _nist: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-132.pdf
 .. _bcrypt: https://en.wikipedia.org/wiki/Bcrypt
@@ -431,10 +430,7 @@ The full list of hashers included in Django is::
         'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
         'django.contrib.auth.hashers.BCryptPasswordHasher',
         'django.contrib.auth.hashers.ScryptPasswordHasher',
-        'django.contrib.auth.hashers.SHA1PasswordHasher',
         'django.contrib.auth.hashers.MD5PasswordHasher',
-        'django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher',
-        'django.contrib.auth.hashers.UnsaltedMD5PasswordHasher',
     ]
 
 The corresponding algorithm names are:
@@ -445,10 +441,7 @@ The corresponding algorithm names are:
 * ``bcrypt_sha256``
 * ``bcrypt``
 * ``scrypt``
-* ``sha1``
 * ``md5``
-* ``unsalted_sha1``
-* ``unsalted_md5``
 
 .. _write-your-own-password-hasher:
 

+ 34 - 8
tests/auth_tests/test_hashers.py

@@ -11,7 +11,6 @@ from django.contrib.auth.hashers import (
     PBKDF2PasswordHasher,
     PBKDF2SHA1PasswordHasher,
     ScryptPasswordHasher,
-    SHA1PasswordHasher,
     check_password,
     get_hasher,
     identify_hasher,
@@ -20,7 +19,7 @@ from django.contrib.auth.hashers import (
 )
 from django.test import SimpleTestCase, ignore_warnings
 from django.test.utils import override_settings
-from django.utils.deprecation import RemovedInDjango50Warning
+from django.utils.deprecation import RemovedInDjango50Warning, RemovedInDjango51Warning
 
 # RemovedInDjango50Warning.
 try:
@@ -96,6 +95,7 @@ class TestUtilsHashPass(SimpleTestCase):
         self.assertIs(hasher.must_update(encoded_weak_salt), True)
         self.assertIs(hasher.must_update(encoded_strong_salt), False)
 
+    @ignore_warnings(category=RemovedInDjango51Warning)
     @override_settings(
         PASSWORD_HASHERS=["django.contrib.auth.hashers.SHA1PasswordHasher"]
     )
@@ -121,6 +121,14 @@ class TestUtilsHashPass(SimpleTestCase):
         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(
         PASSWORD_HASHERS=["django.contrib.auth.hashers.MD5PasswordHasher"]
     )
@@ -144,6 +152,7 @@ class TestUtilsHashPass(SimpleTestCase):
         self.assertIs(hasher.must_update(encoded_weak_salt), True)
         self.assertIs(hasher.must_update(encoded_strong_salt), False)
 
+    @ignore_warnings(category=RemovedInDjango51Warning)
     @override_settings(
         PASSWORD_HASHERS=["django.contrib.auth.hashers.UnsaltedMD5PasswordHasher"]
     )
@@ -165,6 +174,7 @@ class TestUtilsHashPass(SimpleTestCase):
         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"]
     )
@@ -174,6 +184,15 @@ class TestUtilsHashPass(SimpleTestCase):
         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"]
     )
@@ -194,6 +213,7 @@ class TestUtilsHashPass(SimpleTestCase):
         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"]
     )
@@ -203,6 +223,14 @@ class TestUtilsHashPass(SimpleTestCase):
         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")
+
     @ignore_warnings(category=RemovedInDjango50Warning)
     @skipUnless(crypt, "no crypt module to generate password.")
     @override_settings(
@@ -432,13 +460,13 @@ class TestUtilsHashPass(SimpleTestCase):
     @override_settings(
         PASSWORD_HASHERS=[
             "django.contrib.auth.hashers.PBKDF2PasswordHasher",
-            "django.contrib.auth.hashers.SHA1PasswordHasher",
+            "django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher",
             "django.contrib.auth.hashers.MD5PasswordHasher",
         ],
     )
     def test_upgrade(self):
         self.assertEqual("pbkdf2_sha256", get_hasher("default").algorithm)
-        for algo in ("sha1", "md5"):
+        for algo in ("pbkdf2_sha1", "md5"):
             with self.subTest(algo=algo):
                 encoded = make_password("lètmein", hasher=algo)
                 state = {"upgraded": False}
@@ -462,13 +490,13 @@ class TestUtilsHashPass(SimpleTestCase):
     @override_settings(
         PASSWORD_HASHERS=[
             "django.contrib.auth.hashers.PBKDF2PasswordHasher",
-            "django.contrib.auth.hashers.SHA1PasswordHasher",
+            "django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher",
             "django.contrib.auth.hashers.MD5PasswordHasher",
         ],
     )
     def test_no_upgrade_on_incorrect_pass(self):
         self.assertEqual("pbkdf2_sha256", get_hasher("default").algorithm)
-        for algo in ("sha1", "md5"):
+        for algo in ("pbkdf2_sha1", "md5"):
             with self.subTest(algo=algo):
                 encoded = make_password("lètmein", hasher=algo)
                 state = {"upgraded": False}
@@ -583,7 +611,6 @@ class TestUtilsHashPass(SimpleTestCase):
             PBKDF2PasswordHasher,
             PBKDF2SHA1PasswordHasher,
             ScryptPasswordHasher,
-            SHA1PasswordHasher,
         ]
         msg = "salt must be provided and cannot contain $."
         for hasher_class in hasher_classes:
@@ -599,7 +626,6 @@ class TestUtilsHashPass(SimpleTestCase):
             PBKDF2PasswordHasher,
             PBKDF2SHA1PasswordHasher,
             ScryptPasswordHasher,
-            SHA1PasswordHasher,
         ]
         msg = "password must be provided."
         for hasher_class in hasher_classes: