Refs #33691 -- Deprecated insecure password hashers.

SHA1PasswordHasher, UnsaltedSHA1PasswordHasher, and UnsaltedMD5PasswordHasher
are now deprecated.
This commit is contained in:
Claude Paroz 2022-07-23 12:45:24 +02:00 committed by Mariusz Felisiak
parent a46dfa87d0
commit 3b79dab19a
5 changed files with 89 additions and 35 deletions

View File

@ -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 ""

View File

@ -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

View File

@ -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.

View File

@ -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:

View File

@ -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: