diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 8eef00f2c1..06b525d355 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -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 = [] diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index ab710fffac..47024514ca 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -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 ` + 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`` diff --git a/docs/releases/1.10.txt b/docs/releases/1.10.txt index b3f14731d3..db6b730670 100644 --- a/docs/releases/1.10.txt +++ b/docs/releases/1.10.txt @@ -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 ` 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 ------------- diff --git a/docs/topics/auth/passwords.txt b/docs/topics/auth/passwords.txt index 17d47d8a0b..ecddeddbfa 100644 --- a/docs/topics/auth/passwords.txt +++ b/docs/topics/auth/passwords.txt @@ -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 `. 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) diff --git a/tests/auth_tests/test_hashers.py b/tests/auth_tests/test_hashers.py index 6a14ba8140..d79a246276 100644 --- a/tests/auth_tests/test_hashers.py +++ b/tests/auth_tests/test_hashers.py @@ -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'):