Fixed #19758 -- Avoided leaking email existence through the password reset form.

This commit is contained in:
Horst Gutmann 2013-02-23 13:39:21 +01:00 committed by Aymeric Augustin
parent 7acabbb980
commit 2f4a4703e1
5 changed files with 47 additions and 36 deletions

View File

@ -14,6 +14,8 @@
<h1>{% trans 'Password reset successful' %}</h1>
<p>{% trans "We've emailed you instructions for setting your password to the email address you submitted. You should be receiving it shortly." %}</p>
<p>{% trans "We've emailed you instructions for setting your password. You should be receiving them shortly." %}</p>
<p>{% trans "If you don't receive an email, please make sure you've entered the address you registered with, and check your spam folder." %}</p>
{% endblock %}

View File

@ -206,31 +206,8 @@ class AuthenticationForm(forms.Form):
class PasswordResetForm(forms.Form):
error_messages = {
'unknown': _("That email address doesn't have an associated "
"user account. Are you sure you've registered?"),
'unusable': _("The user account associated with this email "
"address cannot reset the password."),
}
email = forms.EmailField(label=_("Email"), max_length=254)
def clean_email(self):
"""
Validates that an active user exists with the given email address.
"""
UserModel = get_user_model()
email = self.cleaned_data["email"]
self.users_cache = UserModel._default_manager.filter(email__iexact=email)
if not len(self.users_cache):
raise forms.ValidationError(self.error_messages['unknown'])
if not any(user.is_active for user in self.users_cache):
# none of the filtered users are active
raise forms.ValidationError(self.error_messages['unknown'])
if any((user.password == UNUSABLE_PASSWORD)
for user in self.users_cache):
raise forms.ValidationError(self.error_messages['unusable'])
return email
def save(self, domain_override=None,
subject_template_name='registration/password_reset_subject.txt',
email_template_name='registration/password_reset_email.html',
@ -241,7 +218,14 @@ class PasswordResetForm(forms.Form):
user.
"""
from django.core.mail import send_mail
for user in self.users_cache:
UserModel = get_user_model()
email = self.cleaned_data["email"]
users = UserModel._default_manager.filter(email__iexact=email)
for user in users:
# Make sure that no email is sent to a user that actually has
# a password marked as unusable
if user.password == UNUSABLE_PASSWORD:
continue
if not domain_override:
current_site = get_current_site(request)
site_name = current_site.name

View File

@ -326,20 +326,28 @@ class PasswordResetFormTest(TestCase):
[force_text(EmailField.default_error_messages['invalid'])])
def test_nonexistant_email(self):
# Test nonexistant email address
# Test nonexistant email address. This should not fail because it would
# expose information about registered users.
data = {'email': 'foo@bar.com'}
form = PasswordResetForm(data)
self.assertFalse(form.is_valid())
self.assertEqual(form.errors,
{'email': [force_text(form.error_messages['unknown'])]})
self.assertTrue(form.is_valid())
self.assertEquals(len(mail.outbox), 0)
@override_settings(
TEMPLATE_LOADERS=('django.template.loaders.filesystem.Loader',),
TEMPLATE_DIRS=(
os.path.join(os.path.dirname(upath(__file__)), 'templates'),
),
)
def test_cleaned_data(self):
# Regression test
(user, username, email) = self.create_dummy_user()
data = {'email': email}
form = PasswordResetForm(data)
self.assertTrue(form.is_valid())
form.save(domain_override='example.com')
self.assertEqual(form.cleaned_data['email'], email)
self.assertEqual(len(mail.outbox), 1)
@override_settings(
TEMPLATE_LOADERS=('django.template.loaders.filesystem.Loader',),
@ -373,7 +381,8 @@ class PasswordResetFormTest(TestCase):
user.is_active = False
user.save()
form = PasswordResetForm({'email': email})
self.assertFalse(form.is_valid())
self.assertTrue(form.is_valid())
self.assertEqual(len(mail.outbox), 0)
def test_unusable_password(self):
user = User.objects.create_user('testuser', 'test@example.com', 'test')
@ -383,9 +392,10 @@ class PasswordResetFormTest(TestCase):
user.set_unusable_password()
user.save()
form = PasswordResetForm(data)
self.assertFalse(form.is_valid())
self.assertEqual(form["email"].errors,
[_("The user account associated with this email address cannot reset the password.")])
# The form itself is valid, but no email is sent
self.assertTrue(form.is_valid())
form.save()
self.assertEquals(len(mail.outbox), 0)
class ReadOnlyPasswordHashTest(TestCase):

View File

@ -86,11 +86,12 @@ class AuthViewNamedURLTests(AuthViewsTestCase):
class PasswordResetTest(AuthViewsTestCase):
def test_email_not_found(self):
"Error is raised if the provided email address isn't currently registered"
"""If the provided email is not registered, don't raise any error but
also don't send any email."""
response = self.client.get('/password_reset/')
self.assertEqual(response.status_code, 200)
response = self.client.post('/password_reset/', {'email': 'not_a_real_email@email.com'})
self.assertFormError(response, PasswordResetForm.error_messages['unknown'])
self.assertEqual(response.status_code, 302)
self.assertEqual(len(mail.outbox), 0)
def test_email_found(self):

View File

@ -743,10 +743,24 @@ patterns.
that can be used to reset the password, and sending that link to the
user's registered email address.
If the email address provided does not exist in the system, this view
won't send an email, but the user won't receive any error message either.
This prevents information leaking to potential attackers. If you want to
provide an error message in this case, you can subclass
:class:`~django.contrib.auth.forms.PasswordResetForm` and use the
``password_reset_form`` argument.
Users flagged with an unusable password (see
:meth:`~django.contrib.auth.models.User.set_unusable_password()` aren't
allowed to request a password reset to prevent misuse when using an
external authentication source like LDAP.
external authentication source like LDAP. Note that they won't receive any
error message since this would expose their account's existence but no
mail will be sent either.
.. versionchanged:: 1.6
Previously, error messages indicated whether a given email was
registered.
**URL name:** ``password_reset``