Refs #33691 -- Deprecated insecure password hashers.
SHA1PasswordHasher, UnsaltedSHA1PasswordHasher, and UnsaltedMD5PasswordHasher are now deprecated.
This commit is contained in:
parent
a46dfa87d0
commit
3b79dab19a
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue