Commit 2f4a4703 authored by Horst Gutmann's avatar Horst Gutmann Committed by Aymeric Augustin
Browse files

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

parent 7acabbb9
Loading
Loading
Loading
Loading
+3 −1
Original line number Diff line number Diff line
@@ -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 %}
+8 −24
Original line number Diff line number Diff line
@@ -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
+18 −8
Original line number Diff line number Diff line
@@ -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):
+3 −2
Original line number Diff line number Diff line
@@ -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):
+15 −1
Original line number Diff line number Diff line
@@ -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``