mirror of
				https://github.com/django/django.git
				synced 2025-10-31 01:25:32 +00:00 
			
		
		
		
	Fixed #20079 -- Improve security of password reset tokens
This commit is contained in:
		| @@ -14,7 +14,7 @@ from django.utils.translation import ugettext, ugettext_lazy as _ | |||||||
|  |  | ||||||
| from django.contrib.auth import authenticate, get_user_model | from django.contrib.auth import authenticate, get_user_model | ||||||
| from django.contrib.auth.models import User | from django.contrib.auth.models import User | ||||||
| from django.contrib.auth.hashers import UNUSABLE_PASSWORD, identify_hasher | from django.contrib.auth.hashers import UNUSABLE_PASSWORD_PREFIX, identify_hasher | ||||||
| from django.contrib.auth.tokens import default_token_generator | from django.contrib.auth.tokens import default_token_generator | ||||||
| from django.contrib.sites.models import get_current_site | from django.contrib.sites.models import get_current_site | ||||||
|  |  | ||||||
| @@ -29,7 +29,7 @@ class ReadOnlyPasswordHashWidget(forms.Widget): | |||||||
|         encoded = value |         encoded = value | ||||||
|         final_attrs = self.build_attrs(attrs) |         final_attrs = self.build_attrs(attrs) | ||||||
|  |  | ||||||
|         if not encoded or encoded == UNUSABLE_PASSWORD: |         if not encoded or encoded.startswith(UNUSABLE_PASSWORD_PREFIX): | ||||||
|             summary = mark_safe("<strong>%s</strong>" % ugettext("No password set.")) |             summary = mark_safe("<strong>%s</strong>" % ugettext("No password set.")) | ||||||
|         else: |         else: | ||||||
|             try: |             try: | ||||||
| @@ -231,7 +231,7 @@ class PasswordResetForm(forms.Form): | |||||||
|         for user in users: |         for user in users: | ||||||
|             # Make sure that no email is sent to a user that actually has |             # Make sure that no email is sent to a user that actually has | ||||||
|             # a password marked as unusable |             # a password marked as unusable | ||||||
|             if user.password == UNUSABLE_PASSWORD: |             if not user.has_usable_password(): | ||||||
|                 continue |                 continue | ||||||
|             if not domain_override: |             if not domain_override: | ||||||
|                 current_site = get_current_site(request) |                 current_site = get_current_site(request) | ||||||
|   | |||||||
| @@ -17,7 +17,8 @@ from django.utils.module_loading import import_by_path | |||||||
| from django.utils.translation import ugettext_noop as _ | from django.utils.translation import ugettext_noop as _ | ||||||
|  |  | ||||||
|  |  | ||||||
| UNUSABLE_PASSWORD = '!'  # This will never be a valid encoded hash | UNUSABLE_PASSWORD_PREFIX = '!'  # This will never be a valid encoded hash | ||||||
|  | UNUSABLE_PASSWORD_SUFFIX_LENGTH = 40  # number of random chars to add after UNUSABLE_PASSWORD_PREFIX | ||||||
| HASHERS = None  # lazily loaded from PASSWORD_HASHERS | HASHERS = None  # lazily loaded from PASSWORD_HASHERS | ||||||
| PREFERRED_HASHER = None  # defaults to first item in PASSWORD_HASHERS | PREFERRED_HASHER = None  # defaults to first item in PASSWORD_HASHERS | ||||||
|  |  | ||||||
| @@ -30,7 +31,7 @@ def reset_hashers(**kwargs): | |||||||
|  |  | ||||||
|  |  | ||||||
| def is_password_usable(encoded): | def is_password_usable(encoded): | ||||||
|     if encoded is None or encoded == UNUSABLE_PASSWORD: |     if encoded is None or encoded.startswith(UNUSABLE_PASSWORD_PREFIX): | ||||||
|         return False |         return False | ||||||
|     try: |     try: | ||||||
|         hasher = identify_hasher(encoded) |         hasher = identify_hasher(encoded) | ||||||
| @@ -64,13 +65,15 @@ def make_password(password, salt=None, hasher='default'): | |||||||
|     """ |     """ | ||||||
|     Turn a plain-text password into a hash for database storage |     Turn a plain-text password into a hash for database storage | ||||||
|  |  | ||||||
|     Same as encode() but generates a new random salt.  If |     Same as encode() but generates a new random salt. | ||||||
|     password is None then UNUSABLE_PASSWORD will be |     If password is None then a concatenation of | ||||||
|     returned which disallows logins. |     UNUSABLE_PASSWORD_PREFIX and a random string will be returned | ||||||
|  |     which disallows logins. Additional random string reduces chances | ||||||
|  |     of gaining access to staff or superuser accounts. | ||||||
|  |     See ticket #20079 for more info. | ||||||
|     """ |     """ | ||||||
|     if password is None: |     if password is None: | ||||||
|         return UNUSABLE_PASSWORD |         return UNUSABLE_PASSWORD_PREFIX + get_random_string(UNUSABLE_PASSWORD_SUFFIX_LENGTH) | ||||||
|  |  | ||||||
|     hasher = get_hasher(hasher) |     hasher = get_hasher(hasher) | ||||||
|  |  | ||||||
|     if not salt: |     if not salt: | ||||||
|   | |||||||
| @@ -16,7 +16,7 @@ from django.utils import timezone | |||||||
| from django.contrib import auth | from django.contrib import auth | ||||||
| # UNUSABLE_PASSWORD is still imported here for backwards compatibility | # UNUSABLE_PASSWORD is still imported here for backwards compatibility | ||||||
| from django.contrib.auth.hashers import ( | from django.contrib.auth.hashers import ( | ||||||
|     check_password, make_password, is_password_usable, UNUSABLE_PASSWORD) |     check_password, make_password, is_password_usable) | ||||||
| from django.contrib.auth.signals import user_logged_in | from django.contrib.auth.signals import user_logged_in | ||||||
| from django.contrib.contenttypes.models import ContentType | from django.contrib.contenttypes.models import ContentType | ||||||
| from django.utils.encoding import python_2_unicode_compatible | from django.utils.encoding import python_2_unicode_compatible | ||||||
|   | |||||||
| @@ -3,8 +3,8 @@ from __future__ import unicode_literals | |||||||
|  |  | ||||||
| from django.conf.global_settings import PASSWORD_HASHERS as default_hashers | from django.conf.global_settings import PASSWORD_HASHERS as default_hashers | ||||||
| from django.contrib.auth.hashers import (is_password_usable, BasePasswordHasher, | from django.contrib.auth.hashers import (is_password_usable, BasePasswordHasher, | ||||||
|     check_password, make_password, PBKDF2PasswordHasher, load_hashers, |     check_password, make_password, PBKDF2PasswordHasher, load_hashers, PBKDF2SHA1PasswordHasher, | ||||||
|     PBKDF2SHA1PasswordHasher, get_hasher, identify_hasher, UNUSABLE_PASSWORD) |     get_hasher, identify_hasher, UNUSABLE_PASSWORD_PREFIX, UNUSABLE_PASSWORD_SUFFIX_LENGTH) | ||||||
| from django.utils import six | from django.utils import six | ||||||
| from django.utils import unittest | from django.utils import unittest | ||||||
| from django.utils.unittest import skipUnless | from django.utils.unittest import skipUnless | ||||||
| @@ -173,13 +173,18 @@ class TestUtilsHashPass(unittest.TestCase): | |||||||
|  |  | ||||||
|     def test_unusable(self): |     def test_unusable(self): | ||||||
|         encoded = make_password(None) |         encoded = make_password(None) | ||||||
|  |         self.assertEqual(len(encoded), len(UNUSABLE_PASSWORD_PREFIX) + UNUSABLE_PASSWORD_SUFFIX_LENGTH) | ||||||
|         self.assertFalse(is_password_usable(encoded)) |         self.assertFalse(is_password_usable(encoded)) | ||||||
|         self.assertFalse(check_password(None, encoded)) |         self.assertFalse(check_password(None, encoded)) | ||||||
|         self.assertFalse(check_password(UNUSABLE_PASSWORD, encoded)) |         self.assertFalse(check_password(encoded, encoded)) | ||||||
|  |         self.assertFalse(check_password(UNUSABLE_PASSWORD_PREFIX, encoded)) | ||||||
|         self.assertFalse(check_password('', encoded)) |         self.assertFalse(check_password('', encoded)) | ||||||
|         self.assertFalse(check_password('lètmein', encoded)) |         self.assertFalse(check_password('lètmein', encoded)) | ||||||
|         self.assertFalse(check_password('lètmeinz', encoded)) |         self.assertFalse(check_password('lètmeinz', encoded)) | ||||||
|         self.assertRaises(ValueError, identify_hasher, encoded) |         self.assertRaises(ValueError, identify_hasher, encoded) | ||||||
|  |         # Assert that the unusable passwords actually contain a random part. | ||||||
|  |         # This might fail one day due to a hash collision. | ||||||
|  |         self.assertNotEqual(encoded, make_password(None), "Random password collision?") | ||||||
|  |  | ||||||
|     def test_bad_algorithm(self): |     def test_bad_algorithm(self): | ||||||
|         with self.assertRaises(ValueError): |         with self.assertRaises(ValueError): | ||||||
|   | |||||||
| @@ -87,7 +87,7 @@ class UserManagerTestCase(TestCase): | |||||||
|         user = User.objects.create_user('user', email_lowercase) |         user = User.objects.create_user('user', email_lowercase) | ||||||
|         self.assertEqual(user.email, email_lowercase) |         self.assertEqual(user.email, email_lowercase) | ||||||
|         self.assertEqual(user.username, 'user') |         self.assertEqual(user.username, 'user') | ||||||
|         self.assertEqual(user.password, '!') |         self.assertFalse(user.has_usable_password()) | ||||||
|  |  | ||||||
|     def test_create_user_email_domain_normalize_rfc3696(self): |     def test_create_user_email_domain_normalize_rfc3696(self): | ||||||
|         # According to  http://tools.ietf.org/html/rfc3696#section-3 |         # According to  http://tools.ietf.org/html/rfc3696#section-3 | ||||||
|   | |||||||
| @@ -22,7 +22,7 @@ from django.contrib.admin.util import quote | |||||||
| from django.contrib.admin.views.main import IS_POPUP_VAR | from django.contrib.admin.views.main import IS_POPUP_VAR | ||||||
| from django.contrib.admin.tests import AdminSeleniumWebDriverTestCase | from django.contrib.admin.tests import AdminSeleniumWebDriverTestCase | ||||||
| from django.contrib.auth import REDIRECT_FIELD_NAME | from django.contrib.auth import REDIRECT_FIELD_NAME | ||||||
| from django.contrib.auth.models import Group, User, Permission, UNUSABLE_PASSWORD | from django.contrib.auth.models import Group, User, Permission | ||||||
| from django.contrib.contenttypes.models import ContentType | from django.contrib.contenttypes.models import ContentType | ||||||
| from django.core.urlresolvers import reverse | from django.core.urlresolvers import reverse | ||||||
| from django.db import connection | from django.db import connection | ||||||
| @@ -3632,7 +3632,7 @@ class UserAdminTest(TestCase): | |||||||
|         new_user = User.objects.order_by('-id')[0] |         new_user = User.objects.order_by('-id')[0] | ||||||
|         self.assertRedirects(response, '/test_admin/admin/auth/user/%s/' % new_user.pk) |         self.assertRedirects(response, '/test_admin/admin/auth/user/%s/' % new_user.pk) | ||||||
|         self.assertEqual(User.objects.count(), user_count + 1) |         self.assertEqual(User.objects.count(), user_count + 1) | ||||||
|         self.assertNotEqual(new_user.password, UNUSABLE_PASSWORD) |         self.assertTrue(new_user.has_usable_password()) | ||||||
|  |  | ||||||
|     def test_save_continue_editing_button(self): |     def test_save_continue_editing_button(self): | ||||||
|         user_count = User.objects.count() |         user_count = User.objects.count() | ||||||
| @@ -3645,7 +3645,7 @@ class UserAdminTest(TestCase): | |||||||
|         new_user = User.objects.order_by('-id')[0] |         new_user = User.objects.order_by('-id')[0] | ||||||
|         self.assertRedirects(response, '/test_admin/admin/auth/user/%s/' % new_user.pk) |         self.assertRedirects(response, '/test_admin/admin/auth/user/%s/' % new_user.pk) | ||||||
|         self.assertEqual(User.objects.count(), user_count + 1) |         self.assertEqual(User.objects.count(), user_count + 1) | ||||||
|         self.assertNotEqual(new_user.password, UNUSABLE_PASSWORD) |         self.assertTrue(new_user.has_usable_password()) | ||||||
|  |  | ||||||
|     def test_password_mismatch(self): |     def test_password_mismatch(self): | ||||||
|         response = self.client.post('/test_admin/admin/auth/user/add/', { |         response = self.client.post('/test_admin/admin/auth/user/add/', { | ||||||
| @@ -3691,7 +3691,7 @@ class UserAdminTest(TestCase): | |||||||
|         new_user = User.objects.order_by('-id')[0] |         new_user = User.objects.order_by('-id')[0] | ||||||
|         self.assertRedirects(response, '/test_admin/admin/auth/user/add/') |         self.assertRedirects(response, '/test_admin/admin/auth/user/add/') | ||||||
|         self.assertEqual(User.objects.count(), user_count + 1) |         self.assertEqual(User.objects.count(), user_count + 1) | ||||||
|         self.assertNotEqual(new_user.password, UNUSABLE_PASSWORD) |         self.assertTrue(new_user.has_usable_password()) | ||||||
|  |  | ||||||
|     def test_user_permission_performance(self): |     def test_user_permission_performance(self): | ||||||
|         u = User.objects.all()[0] |         u = User.objects.all()[0] | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user