summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHorst Gutmann <hgutmann@netconomy.net>2013-02-23 13:39:21 +0100
committerAymeric Augustin <aymeric.augustin@m4x.org>2013-02-23 14:31:21 +0100
commit2f4a4703e1931fadf5ed81387b26cf84caf5bef9 (patch)
treefc210f8544d89da63e6b92a1ca4b5a7ea657e678
parent7acabbb9800de4492c0ee612a8a0aa784eb07f49 (diff)
Fixed #19758 -- Avoided leaking email existence through the password reset form.
-rw-r--r--django/contrib/admin/templates/registration/password_reset_done.html4
-rw-r--r--django/contrib/auth/forms.py32
-rw-r--r--django/contrib/auth/tests/forms.py26
-rw-r--r--django/contrib/auth/tests/views.py5
-rw-r--r--docs/topics/auth/default.txt16
5 files changed, 47 insertions, 36 deletions
diff --git a/django/contrib/admin/templates/registration/password_reset_done.html b/django/contrib/admin/templates/registration/password_reset_done.html
index 7584c8393a..98471041b5 100644
--- a/django/contrib/admin/templates/registration/password_reset_done.html
+++ b/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 %}
diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py
index ee4fb482ce..c28971b94d 100644
--- a/django/contrib/auth/forms.py
+++ b/django/contrib/auth/forms.py
@@ -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
diff --git a/django/contrib/auth/tests/forms.py b/django/contrib/auth/tests/forms.py
index c5a3fec7ce..781b917517 100644
--- a/django/contrib/auth/tests/forms.py
+++ b/django/contrib/auth/tests/forms.py
@@ -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):
diff --git a/django/contrib/auth/tests/views.py b/django/contrib/auth/tests/views.py
index 229e294398..b41c7198f5 100644
--- a/django/contrib/auth/tests/views.py
+++ b/django/contrib/auth/tests/views.py
@@ -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):
diff --git a/docs/topics/auth/default.txt b/docs/topics/auth/default.txt
index 1a57770b2b..d82731f73b 100644
--- a/docs/topics/auth/default.txt
+++ b/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``