1
0
mirror of https://github.com/django/django.git synced 2024-12-23 09:36:06 +00:00

Fixed #26187 -- Removed weak password hashers from PASSWORD_HASHERS.

This commit is contained in:
Tim Graham 2016-02-08 14:22:38 -05:00
parent b14470c7b7
commit 47b5a6a43c
5 changed files with 119 additions and 33 deletions

View File

@ -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 = []

View File

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

View File

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

View File

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

View File

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