summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorClaude Paroz <claude@2xlibre.net>2022-07-23 12:45:24 +0200
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2022-07-23 21:29:31 +0200
commit3b79dab19a2300a4884a3d81baa6c7c1f2dee059 (patch)
tree325e7d980634e695bee88b6a1ccb2e48c14660da
parenta46dfa87d0ba690125be98f7f1b77062a1794fdc (diff)
Refs #33691 -- Deprecated insecure password hashers.
SHA1PasswordHasher, UnsaltedSHA1PasswordHasher, and UnsaltedMD5PasswordHasher are now deprecated.
-rw-r--r--django/contrib/auth/hashers.py29
-rw-r--r--docs/internals/deprecation.txt4
-rw-r--r--docs/releases/4.2.txt4
-rw-r--r--docs/topics/auth/passwords.txt45
-rw-r--r--tests/auth_tests/test_hashers.py42
5 files changed, 89 insertions, 35 deletions
diff --git a/django/contrib/auth/hashers.py b/django/contrib/auth/hashers.py
index 10c9079200..432c624483 100644
--- a/django/contrib/auth/hashers.py
+++ b/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 ""
diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt
index 43c7a0fee9..80597eb95b 100644
--- a/docs/internals/deprecation.txt
+++ b/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
diff --git a/docs/releases/4.2.txt b/docs/releases/4.2.txt
index 0846f05e2c..1d55ea2f68 100644
--- a/docs/releases/4.2.txt
+++ b/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.
diff --git a/docs/topics/auth/passwords.txt b/docs/topics/auth/passwords.txt
index 3836ab4006..25c98bf786 100644
--- a/docs/topics/auth/passwords.txt
+++ b/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:
diff --git a/tests/auth_tests/test_hashers.py b/tests/auth_tests/test_hashers.py
index d0965fb6c1..a5dfd51a6b 100644
--- a/tests/auth_tests/test_hashers.py
+++ b/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"]
)
@@ -122,6 +122,14 @@ class TestUtilsHashPass(SimpleTestCase):
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"]
)
def test_md5(self):
@@ -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"]
)
@@ -175,6 +185,15 @@ class TestUtilsHashPass(SimpleTestCase):
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):
@@ -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: