Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Fixed #19758 -- Avoided leaking email existence through the password …

…reset form.
  • Loading branch information...
commit 2f4a4703e1931fadf5ed81387b26cf84caf5bef9 1 parent 7acabbb
@zerok zerok authored aaugustin committed
View
4 django/contrib/admin/templates/registration/password_reset_done.html
@@ -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
32 django/contrib/auth/forms.py
@@ -206,31 +206,8 @@ def get_user(self):
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 @@ def save(self, domain_override=None,
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
26 django/contrib/auth/tests/forms.py
@@ -326,20 +326,28 @@ def test_invalid_email(self):
[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 @@ def test_inactive_user(self):
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 @@ def test_unusable_password(self):
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
5 django/contrib/auth/tests/views.py
@@ -86,11 +86,12 @@ def test_named_urls(self):
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
16 docs/topics/auth/default.txt
@@ -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``

0 comments on commit 2f4a470

Please sign in to comment.
Something went wrong with that request. Please try again.