Fixed #26187 -- Removed weak password hashers from PASSWORD_HASHERS.
This commit is contained in:
parent
b14470c7b7
commit
47b5a6a43c
|
@ -502,11 +502,6 @@ PASSWORD_HASHERS = [
|
||||||
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
|
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
|
||||||
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
|
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
|
||||||
'django.contrib.auth.hashers.BCryptPasswordHasher',
|
'django.contrib.auth.hashers.BCryptPasswordHasher',
|
||||||
'django.contrib.auth.hashers.SHA1PasswordHasher',
|
|
||||||
'django.contrib.auth.hashers.MD5PasswordHasher',
|
|
||||||
'django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher',
|
|
||||||
'django.contrib.auth.hashers.UnsaltedMD5PasswordHasher',
|
|
||||||
'django.contrib.auth.hashers.CryptPasswordHasher',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
AUTH_PASSWORD_VALIDATORS = []
|
AUTH_PASSWORD_VALIDATORS = []
|
||||||
|
|
|
@ -2686,13 +2686,22 @@ Default::
|
||||||
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
|
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
|
||||||
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
|
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
|
||||||
'django.contrib.auth.hashers.BCryptPasswordHasher',
|
'django.contrib.auth.hashers.BCryptPasswordHasher',
|
||||||
'django.contrib.auth.hashers.SHA1PasswordHasher',
|
|
||||||
'django.contrib.auth.hashers.MD5PasswordHasher',
|
|
||||||
'django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher',
|
|
||||||
'django.contrib.auth.hashers.UnsaltedMD5PasswordHasher',
|
|
||||||
'django.contrib.auth.hashers.CryptPasswordHasher',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
.. versionchanged:: 1.10
|
||||||
|
|
||||||
|
The following hashers were removed from the defaults::
|
||||||
|
|
||||||
|
'django.contrib.auth.hashers.SHA1PasswordHasher'
|
||||||
|
'django.contrib.auth.hashers.MD5PasswordHasher'
|
||||||
|
'django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher'
|
||||||
|
'django.contrib.auth.hashers.UnsaltedMD5PasswordHasher'
|
||||||
|
'django.contrib.auth.hashers.CryptPasswordHasher'
|
||||||
|
|
||||||
|
Consider using a :ref:`wrapped password hasher <wrapping-password-hashers>`
|
||||||
|
to strengthen the hashes in your database. If that's not feasible, add this
|
||||||
|
setting to your project and add back any hashers that you need.
|
||||||
|
|
||||||
.. setting:: AUTH_PASSWORD_VALIDATORS
|
.. setting:: AUTH_PASSWORD_VALIDATORS
|
||||||
|
|
||||||
``AUTH_PASSWORD_VALIDATORS``
|
``AUTH_PASSWORD_VALIDATORS``
|
||||||
|
|
|
@ -502,6 +502,50 @@ In older versions, assigning ``None`` to a non-nullable ``ForeignKey`` or
|
||||||
not allow null values.')``. For consistency with other model fields which don't
|
not allow null values.')``. For consistency with other model fields which don't
|
||||||
have a similar check, this check is removed.
|
have a similar check, this check is removed.
|
||||||
|
|
||||||
|
Removed weak password hashers from the default ``PASSWORD_HASHERS`` setting
|
||||||
|
---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Django 0.90 stored passwords as unsalted MD5. Django 0.91 added support for
|
||||||
|
salted SHA1 with automatic upgrade of passwords when a user logs in. Django 1.4
|
||||||
|
added PBKDF2 as the default password hasher.
|
||||||
|
|
||||||
|
If you have an old Django project with MD5 or SHA1 (even salted) encoded
|
||||||
|
passwords, be aware that these can be cracked fairly easily with today's
|
||||||
|
hardware. To make Django users acknowledge continued use of weak hashers, the
|
||||||
|
following hashers are removed from the default :setting:`PASSWORD_HASHERS`
|
||||||
|
setting::
|
||||||
|
|
||||||
|
'django.contrib.auth.hashers.SHA1PasswordHasher'
|
||||||
|
'django.contrib.auth.hashers.MD5PasswordHasher'
|
||||||
|
'django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher'
|
||||||
|
'django.contrib.auth.hashers.UnsaltedMD5PasswordHasher'
|
||||||
|
'django.contrib.auth.hashers.CryptPasswordHasher'
|
||||||
|
|
||||||
|
Consider using a :ref:`wrapped password hasher <wrapping-password-hashers>` to
|
||||||
|
strengthen the hashes in your database. If that's not feasible, add the
|
||||||
|
:setting:`PASSWORD_HASHERS` setting to your project and add back any hashers
|
||||||
|
that you need.
|
||||||
|
|
||||||
|
You can check if your database has any of the removed hashers like this::
|
||||||
|
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
# Unsalted MD5/SHA1:
|
||||||
|
User.objects.filter(password__startswith='md5$$')
|
||||||
|
User.objects.filter(password__startswith='sha1$$')
|
||||||
|
# Salted MD5/SHA1:
|
||||||
|
User.objects.filter(password__startswith='md5$').exclude(password__startswith='md5$$')
|
||||||
|
User.objects.filter(password__startswith='sha1$').exclude(password__startswith='sha1$$')
|
||||||
|
# Crypt hasher:
|
||||||
|
User.objects.filter(password__startswith='crypt$$')
|
||||||
|
|
||||||
|
from django.db.models import CharField
|
||||||
|
from django.db.models.functions import Length
|
||||||
|
CharField.register_lookup(Length)
|
||||||
|
# Unsalted MD5 passwords might not have an 'md5$$' prefix:
|
||||||
|
User.objects.filter(password__length=32)
|
||||||
|
|
||||||
Miscellaneous
|
Miscellaneous
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
|
|
|
@ -62,15 +62,13 @@ The default for :setting:`PASSWORD_HASHERS` is::
|
||||||
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
|
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
|
||||||
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
|
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
|
||||||
'django.contrib.auth.hashers.BCryptPasswordHasher',
|
'django.contrib.auth.hashers.BCryptPasswordHasher',
|
||||||
'django.contrib.auth.hashers.SHA1PasswordHasher',
|
|
||||||
'django.contrib.auth.hashers.MD5PasswordHasher',
|
|
||||||
'django.contrib.auth.hashers.CryptPasswordHasher',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
This means that Django will use PBKDF2_ to store all passwords, but will support
|
This means that Django will use PBKDF2_ to store all passwords but will support
|
||||||
checking passwords stored with PBKDF2SHA1, bcrypt_, SHA1_, etc. The next few
|
checking passwords stored with PBKDF2SHA1 and bcrypt_.
|
||||||
sections describe a couple of common ways advanced users may want to modify this
|
|
||||||
setting.
|
The next few sections describe a couple of common ways advanced users may want
|
||||||
|
to modify this setting.
|
||||||
|
|
||||||
.. _bcrypt_usage:
|
.. _bcrypt_usage:
|
||||||
|
|
||||||
|
@ -96,13 +94,10 @@ To use Bcrypt as your default storage algorithm, do the following:
|
||||||
'django.contrib.auth.hashers.BCryptPasswordHasher',
|
'django.contrib.auth.hashers.BCryptPasswordHasher',
|
||||||
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
|
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
|
||||||
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
|
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
|
||||||
'django.contrib.auth.hashers.SHA1PasswordHasher',
|
|
||||||
'django.contrib.auth.hashers.MD5PasswordHasher',
|
|
||||||
'django.contrib.auth.hashers.CryptPasswordHasher',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
(You need to keep the other entries in this list, or else Django won't
|
Keep and/or add any entries in this list if you need Django to :ref:`upgrade
|
||||||
be able to upgrade passwords; see below).
|
passwords <password-upgrades>`.
|
||||||
|
|
||||||
That's it -- now your Django install will use Bcrypt as the default storage
|
That's it -- now your Django install will use Bcrypt as the default storage
|
||||||
algorithm.
|
algorithm.
|
||||||
|
@ -168,12 +163,8 @@ default PBKDF2 algorithm:
|
||||||
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
|
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
|
||||||
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
|
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
|
||||||
'django.contrib.auth.hashers.BCryptPasswordHasher',
|
'django.contrib.auth.hashers.BCryptPasswordHasher',
|
||||||
'django.contrib.auth.hashers.SHA1PasswordHasher',
|
|
||||||
'django.contrib.auth.hashers.MD5PasswordHasher',
|
|
||||||
'django.contrib.auth.hashers.CryptPasswordHasher',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
That's it -- now your Django install will use more iterations when it
|
That's it -- now your Django install will use more iterations when it
|
||||||
stores passwords using PBKDF2.
|
stores passwords using PBKDF2.
|
||||||
|
|
||||||
|
@ -288,6 +279,37 @@ Include any other hashers that your site uses in this list.
|
||||||
.. _bcrypt: https://en.wikipedia.org/wiki/Bcrypt
|
.. _bcrypt: https://en.wikipedia.org/wiki/Bcrypt
|
||||||
.. _`bcrypt library`: https://pypi.python.org/pypi/bcrypt/
|
.. _`bcrypt library`: https://pypi.python.org/pypi/bcrypt/
|
||||||
|
|
||||||
|
.. _auth-included-hashers:
|
||||||
|
|
||||||
|
Included hashers
|
||||||
|
----------------
|
||||||
|
|
||||||
|
The full list of hashers included in Django is::
|
||||||
|
|
||||||
|
[
|
||||||
|
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
|
||||||
|
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
|
||||||
|
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
|
||||||
|
'django.contrib.auth.hashers.BCryptPasswordHasher',
|
||||||
|
'django.contrib.auth.hashers.SHA1PasswordHasher',
|
||||||
|
'django.contrib.auth.hashers.MD5PasswordHasher',
|
||||||
|
'django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher',
|
||||||
|
'django.contrib.auth.hashers.UnsaltedMD5PasswordHasher',
|
||||||
|
'django.contrib.auth.hashers.CryptPasswordHasher',
|
||||||
|
]
|
||||||
|
|
||||||
|
The corresponding algorithm names are:
|
||||||
|
|
||||||
|
* ``pbkdf2_sha256``
|
||||||
|
* ``pbkdf2_sha1``
|
||||||
|
* ``bcrypt_sha256``
|
||||||
|
* ``bcrypt``
|
||||||
|
* ``sha1``
|
||||||
|
* ``md5``
|
||||||
|
* ``unsalted_sha1``
|
||||||
|
* ``unsalted_md5``
|
||||||
|
* ``crypt``
|
||||||
|
|
||||||
Manually managing a user's password
|
Manually managing a user's password
|
||||||
===================================
|
===================================
|
||||||
|
|
||||||
|
@ -311,13 +333,10 @@ from the ``User`` model.
|
||||||
Creates a hashed password in the format used by this application. It takes
|
Creates a hashed password in the format used by this application. It takes
|
||||||
one mandatory argument: the password in plain-text. Optionally, you can
|
one mandatory argument: the password in plain-text. Optionally, you can
|
||||||
provide a salt and a hashing algorithm to use, if you don't want to use the
|
provide a salt and a hashing algorithm to use, if you don't want to use the
|
||||||
defaults (first entry of ``PASSWORD_HASHERS`` setting).
|
defaults (first entry of ``PASSWORD_HASHERS`` setting). See
|
||||||
Currently supported algorithms are: ``'pbkdf2_sha256'``, ``'pbkdf2_sha1'``,
|
:ref:`auth-included-hashers` for the algorithm name of each hasher. If the
|
||||||
``'bcrypt_sha256'`` (see :ref:`bcrypt_usage`), ``'bcrypt'``, ``'sha1'``,
|
password argument is ``None``, an unusable password is returned (a one that
|
||||||
``'md5'``, ``'unsalted_md5'`` (only for backward compatibility) and ``'crypt'``
|
will be never accepted by :func:`check_password`).
|
||||||
if you have the ``crypt`` library installed. If the password argument is
|
|
||||||
``None``, an unusable password is returned (a one that will be never
|
|
||||||
accepted by :func:`check_password`).
|
|
||||||
|
|
||||||
.. function:: is_password_usable(encoded_password)
|
.. function:: is_password_usable(encoded_password)
|
||||||
|
|
||||||
|
|
|
@ -60,6 +60,7 @@ class TestUtilsHashPass(SimpleTestCase):
|
||||||
self.assertTrue(check_password('', blank_encoded))
|
self.assertTrue(check_password('', blank_encoded))
|
||||||
self.assertFalse(check_password(' ', blank_encoded))
|
self.assertFalse(check_password(' ', blank_encoded))
|
||||||
|
|
||||||
|
@override_settings(PASSWORD_HASHERS=['django.contrib.auth.hashers.SHA1PasswordHasher'])
|
||||||
def test_sha1(self):
|
def test_sha1(self):
|
||||||
encoded = make_password('lètmein', 'seasalt', 'sha1')
|
encoded = make_password('lètmein', 'seasalt', 'sha1')
|
||||||
self.assertEqual(encoded,
|
self.assertEqual(encoded,
|
||||||
|
@ -75,6 +76,7 @@ class TestUtilsHashPass(SimpleTestCase):
|
||||||
self.assertTrue(check_password('', blank_encoded))
|
self.assertTrue(check_password('', blank_encoded))
|
||||||
self.assertFalse(check_password(' ', blank_encoded))
|
self.assertFalse(check_password(' ', blank_encoded))
|
||||||
|
|
||||||
|
@override_settings(PASSWORD_HASHERS=['django.contrib.auth.hashers.MD5PasswordHasher'])
|
||||||
def test_md5(self):
|
def test_md5(self):
|
||||||
encoded = make_password('lètmein', 'seasalt', 'md5')
|
encoded = make_password('lètmein', 'seasalt', 'md5')
|
||||||
self.assertEqual(encoded,
|
self.assertEqual(encoded,
|
||||||
|
@ -90,6 +92,7 @@ class TestUtilsHashPass(SimpleTestCase):
|
||||||
self.assertTrue(check_password('', blank_encoded))
|
self.assertTrue(check_password('', blank_encoded))
|
||||||
self.assertFalse(check_password(' ', blank_encoded))
|
self.assertFalse(check_password(' ', blank_encoded))
|
||||||
|
|
||||||
|
@override_settings(PASSWORD_HASHERS=['django.contrib.auth.hashers.UnsaltedMD5PasswordHasher'])
|
||||||
def test_unsalted_md5(self):
|
def test_unsalted_md5(self):
|
||||||
encoded = make_password('lètmein', '', 'unsalted_md5')
|
encoded = make_password('lètmein', '', 'unsalted_md5')
|
||||||
self.assertEqual(encoded, '88a434c88cca4e900f7874cd98123f43')
|
self.assertEqual(encoded, '88a434c88cca4e900f7874cd98123f43')
|
||||||
|
@ -108,6 +111,7 @@ class TestUtilsHashPass(SimpleTestCase):
|
||||||
self.assertTrue(check_password('', blank_encoded))
|
self.assertTrue(check_password('', blank_encoded))
|
||||||
self.assertFalse(check_password(' ', blank_encoded))
|
self.assertFalse(check_password(' ', blank_encoded))
|
||||||
|
|
||||||
|
@override_settings(PASSWORD_HASHERS=['django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher'])
|
||||||
def test_unsalted_sha1(self):
|
def test_unsalted_sha1(self):
|
||||||
encoded = make_password('lètmein', '', 'unsalted_sha1')
|
encoded = make_password('lètmein', '', 'unsalted_sha1')
|
||||||
self.assertEqual(encoded, 'sha1$$6d138ca3ae545631b3abd71a4f076ce759c5700b')
|
self.assertEqual(encoded, 'sha1$$6d138ca3ae545631b3abd71a4f076ce759c5700b')
|
||||||
|
@ -126,6 +130,7 @@ class TestUtilsHashPass(SimpleTestCase):
|
||||||
self.assertFalse(check_password(' ', blank_encoded))
|
self.assertFalse(check_password(' ', blank_encoded))
|
||||||
|
|
||||||
@skipUnless(crypt, "no crypt module to generate password.")
|
@skipUnless(crypt, "no crypt module to generate password.")
|
||||||
|
@override_settings(PASSWORD_HASHERS=['django.contrib.auth.hashers.CryptPasswordHasher'])
|
||||||
def test_crypt(self):
|
def test_crypt(self):
|
||||||
encoded = make_password('lètmei', 'ab', 'crypt')
|
encoded = make_password('lètmei', 'ab', 'crypt')
|
||||||
self.assertEqual(encoded, 'crypt$$ab1Hv2Lg7ltQo')
|
self.assertEqual(encoded, 'crypt$$ab1Hv2Lg7ltQo')
|
||||||
|
@ -256,6 +261,13 @@ class TestUtilsHashPass(SimpleTestCase):
|
||||||
'pbkdf2_sha1$30000$seasalt2$pMzU1zNPcydf6wjnJFbiVKwgULc=')
|
'pbkdf2_sha1$30000$seasalt2$pMzU1zNPcydf6wjnJFbiVKwgULc=')
|
||||||
self.assertTrue(hasher.verify('lètmein', encoded))
|
self.assertTrue(hasher.verify('lètmein', encoded))
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
PASSWORD_HASHERS=[
|
||||||
|
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
|
||||||
|
'django.contrib.auth.hashers.SHA1PasswordHasher',
|
||||||
|
'django.contrib.auth.hashers.MD5PasswordHasher',
|
||||||
|
],
|
||||||
|
)
|
||||||
def test_upgrade(self):
|
def test_upgrade(self):
|
||||||
self.assertEqual('pbkdf2_sha256', get_hasher('default').algorithm)
|
self.assertEqual('pbkdf2_sha256', get_hasher('default').algorithm)
|
||||||
for algo in ('sha1', 'md5'):
|
for algo in ('sha1', 'md5'):
|
||||||
|
@ -276,6 +288,13 @@ class TestUtilsHashPass(SimpleTestCase):
|
||||||
self.assertFalse(check_password('WRONG', encoded, setter))
|
self.assertFalse(check_password('WRONG', encoded, setter))
|
||||||
self.assertFalse(state['upgraded'])
|
self.assertFalse(state['upgraded'])
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
PASSWORD_HASHERS=[
|
||||||
|
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
|
||||||
|
'django.contrib.auth.hashers.SHA1PasswordHasher',
|
||||||
|
'django.contrib.auth.hashers.MD5PasswordHasher',
|
||||||
|
],
|
||||||
|
)
|
||||||
def test_no_upgrade_on_incorrect_pass(self):
|
def test_no_upgrade_on_incorrect_pass(self):
|
||||||
self.assertEqual('pbkdf2_sha256', get_hasher('default').algorithm)
|
self.assertEqual('pbkdf2_sha256', get_hasher('default').algorithm)
|
||||||
for algo in ('sha1', 'md5'):
|
for algo in ('sha1', 'md5'):
|
||||||
|
|
Loading…
Reference in New Issue