Commit 47b5a6a4 authored by Tim Graham's avatar Tim Graham
Browse files

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

parent b14470c7
Loading
Loading
Loading
Loading
+0 −5
Original line number Diff line number Diff line
@@ -502,11 +502,6 @@ PASSWORD_HASHERS = [
    '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',
]

AUTH_PASSWORD_VALIDATORS = []
+14 −5
Original line number Diff line number Diff line
@@ -2686,13 +2686,22 @@ Default::
        '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',
    ]

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

``AUTH_PASSWORD_VALIDATORS``
+44 −0
Original line number Diff line number Diff line
@@ -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
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
-------------

+42 −23
Original line number Diff line number Diff line
@@ -62,15 +62,13 @@ The default for :setting:`PASSWORD_HASHERS` is::
        '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.CryptPasswordHasher',
    ]

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
sections describe a couple of common ways advanced users may want to modify this
setting.
This means that Django will use PBKDF2_ to store all passwords but will support
checking passwords stored with PBKDF2SHA1 and bcrypt_.

The next few sections describe a couple of common ways advanced users may want
to modify this setting.

.. _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.PBKDF2PasswordHasher',
            '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
   be able to upgrade passwords; see below).
   Keep and/or add any entries in this list if you need Django to :ref:`upgrade
   passwords <password-upgrades>`.

That's it -- now your Django install will use Bcrypt as the default storage
algorithm.
@@ -168,12 +163,8 @@ default PBKDF2 algorithm:
            '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.CryptPasswordHasher',
        ]


That's it -- now your Django install will use more iterations when it
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 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
===================================

@@ -311,13 +333,10 @@ from the ``User`` model.
    Creates a hashed password in the format used by this application. It takes
    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
    defaults (first entry of ``PASSWORD_HASHERS`` setting).
    Currently supported algorithms are: ``'pbkdf2_sha256'``, ``'pbkdf2_sha1'``,
    ``'bcrypt_sha256'`` (see :ref:`bcrypt_usage`), ``'bcrypt'``, ``'sha1'``,
    ``'md5'``, ``'unsalted_md5'`` (only for backward compatibility) and ``'crypt'``
    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`).
    defaults (first entry of ``PASSWORD_HASHERS`` setting). See
    :ref:`auth-included-hashers` for the algorithm name of each hasher. 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)

+19 −0
Original line number Diff line number Diff line
@@ -60,6 +60,7 @@ class TestUtilsHashPass(SimpleTestCase):
        self.assertTrue(check_password('', blank_encoded))
        self.assertFalse(check_password(' ', blank_encoded))

    @override_settings(PASSWORD_HASHERS=['django.contrib.auth.hashers.SHA1PasswordHasher'])
    def test_sha1(self):
        encoded = make_password('lètmein', 'seasalt', 'sha1')
        self.assertEqual(encoded,
@@ -75,6 +76,7 @@ class TestUtilsHashPass(SimpleTestCase):
        self.assertTrue(check_password('', blank_encoded))
        self.assertFalse(check_password(' ', blank_encoded))

    @override_settings(PASSWORD_HASHERS=['django.contrib.auth.hashers.MD5PasswordHasher'])
    def test_md5(self):
        encoded = make_password('lètmein', 'seasalt', 'md5')
        self.assertEqual(encoded,
@@ -90,6 +92,7 @@ class TestUtilsHashPass(SimpleTestCase):
        self.assertTrue(check_password('', blank_encoded))
        self.assertFalse(check_password(' ', blank_encoded))

    @override_settings(PASSWORD_HASHERS=['django.contrib.auth.hashers.UnsaltedMD5PasswordHasher'])
    def test_unsalted_md5(self):
        encoded = make_password('lètmein', '', 'unsalted_md5')
        self.assertEqual(encoded, '88a434c88cca4e900f7874cd98123f43')
@@ -108,6 +111,7 @@ class TestUtilsHashPass(SimpleTestCase):
        self.assertTrue(check_password('', blank_encoded))
        self.assertFalse(check_password(' ', blank_encoded))

    @override_settings(PASSWORD_HASHERS=['django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher'])
    def test_unsalted_sha1(self):
        encoded = make_password('lètmein', '', 'unsalted_sha1')
        self.assertEqual(encoded, 'sha1$$6d138ca3ae545631b3abd71a4f076ce759c5700b')
@@ -126,6 +130,7 @@ class TestUtilsHashPass(SimpleTestCase):
        self.assertFalse(check_password(' ', blank_encoded))

    @skipUnless(crypt, "no crypt module to generate password.")
    @override_settings(PASSWORD_HASHERS=['django.contrib.auth.hashers.CryptPasswordHasher'])
    def test_crypt(self):
        encoded = make_password('lètmei', 'ab', 'crypt')
        self.assertEqual(encoded, 'crypt$$ab1Hv2Lg7ltQo')
@@ -256,6 +261,13 @@ class TestUtilsHashPass(SimpleTestCase):
            'pbkdf2_sha1$30000$seasalt2$pMzU1zNPcydf6wjnJFbiVKwgULc=')
        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):
        self.assertEqual('pbkdf2_sha256', get_hasher('default').algorithm)
        for algo in ('sha1', 'md5'):
@@ -276,6 +288,13 @@ class TestUtilsHashPass(SimpleTestCase):
        self.assertFalse(check_password('WRONG', encoded, setter))
        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):
        self.assertEqual('pbkdf2_sha256', get_hasher('default').algorithm)
        for algo in ('sha1', 'md5'):