Opened 4 years ago

Closed 4 years ago

#31139 closed Cleanup/optimization (wontfix)

PasswordResetView return success message for emails not in database also

Reported by: SANYAM MITTAL Owned by: sanyam19092000
Component: contrib.auth Version: dev
Severity: Normal Keywords:
Cc: Triage Stage: Unreviewed
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

PasswordResetView returns a success message for emails not in database also.

Problems Faced

  1. If the user is not Registered but strongly thinks they are registered and have forgotten the password they would keep trying to get Reset email.
  2. If they've typed a wrong email in PasswordResetForm. They would be expecting a reset email with reset URL but wouldn't receive any mail nor any Validation Error would be raised.

Reference:
https://github.com/django/django/blob/0f843fdd5b9b2f2307148465cd60f4e1b2befbb4/django/contrib/auth/views.py#L208

Current PasswordResetForm being used:
https://github.com/django/django/blob/0f843fdd5b9b2f2307148465cd60f4e1b2befbb4/django/contrib/auth/forms.py#L250

Here while saving the form by - https://github.com/django/django/blob/0f843fdd5b9b2f2307148465cd60f4e1b2befbb4/django/contrib/auth/forms.py#L292

 for user in self.get_users(email):
            user_email = getattr(user, email_field_name)
            context = {
                'email': user_email,
                'domain': domain,
                'site_name': site_name,
                'uid': urlsafe_base64_encode(force_bytes(user.pk)),
                'user': user,
                'token': token_generator.make_token(user),
                'protocol': 'https' if use_https else 'http',
                **(extra_email_context or {}),
            }
            self.send_mail(
                subject_template_name, email_template_name, context, from_email,
                user_email, html_email_template_name=html_email_template_name,
            )

It just mails the User (if existing) but doesn't raises an Validation Error if User is not found. Rather it redirects to Success URL.
If we raise a Validation Error when User query set is empty.

We can use def clean_email method
Or can add if else statement before iterating in Available Active Users

Change History (3)

comment:1 by Claude Paroz, 4 years ago

Resolution: wontfix
Status: assignedclosed

Thanks for the report, but this is by design. Please read the documentation which also gives a hint about how to change the behavior.
https://docs.djangoproject.com/en/stable/topics/auth/default/#django.contrib.auth.views.PasswordResetView

comment:2 by SANYAM MITTAL, 4 years ago

Needs documentation: set
Resolution: wontfix
Status: closednew

Thanks for the Reference,

As mentioned in documentation
[​https://docs.djangoproject.com/en/stable/topics/auth/default/#django.contrib.auth.views.PasswordResetView]

This prevents information leaking to potential attackers

Facebook also raises a Validation Error when non registered email is entered
https://www.facebook.com/login/identify/?ctx=recover


Although a potential attacker can easily get these information from Sign-Up/Register page as Validation error is raised when a Duplicate Email Address is entered during sign-up.

If there's not a Unique email Validation during Sign-up there are chances that multiple users get registered with same email (if user mistakenly types someone else's email) and Password Reset email is sent multiple times for different Users which is more risky.


If still it prevents Information leak by any other ways then documentation should be more accurate and should have a sample or Suggestion method to achieve unique email Validation with current PasswordResetForm like

If you want to provide an error message in this case, you can subclass PasswordResetForm and use the form_class attribute.

class PasswordResetForm(forms.Form):
    error_messages = {
        'not_registered': _('Email ID is not Registered'),
    }
    email = forms.EmailField(
        label=_("Email"),
        max_length=254,
        widget=forms.EmailInput(attrs={'autocomplete': 'email'})
    )

    def clean_email(self):
        email = self.cleaned_data.get('email')
        users =  self.get_users(email=email)
        flag = False
        for user in users:
            if user:
                flag = True

        if not flag:
            raise forms.ValidationError(
                self.error_messages['not_registered'],
                code='not_registered',
            )
        return email


    def send_mail(self, subject_template_name, email_template_name,
                  context, from_email, to_email, html_email_template_name=None):
        """
        Send a django.core.mail.EmailMultiAlternatives to `to_email`.
        """
        subject = loader.render_to_string(subject_template_name, context)
        # Email subject *must not* contain newlines
        subject = 'Testing Purpose'
        body = loader.render_to_string(email_template_name, context)

        email_message = EmailMultiAlternatives(subject, body, from_email, [to_email])
        if html_email_template_name is not None:
            html_email = loader.render_to_string(html_email_template_name, context)
            email_message.attach_alternative(html_email, 'text/html')

        email_message.send()

    def get_users(self, email):
        """Given an email, return matching user(s) who should receive a reset.

        This allows subclasses to more easily customize the default policies
        that prevent inactive users and users with unusable passwords from
        resetting their password.
        """
        email_field_name = UserModel.get_email_field_name()
        active_users = UserModel._default_manager.filter(**{
            '%s__iexact' % email_field_name: email,
            'is_active': True,
        })

        return (
            u for u in active_users
            if u.has_usable_password() and
            _unicode_ci_compare(email, getattr(u, email_field_name))
        )

    def save(self, domain_override=None,
             subject_template_name='registration/password_reset_subject.txt',
             email_template_name='registration/password_reset_email.html',
             use_https=False, token_generator=default_token_generator,
             from_email=None, request=None, html_email_template_name=None,
             extra_email_context=None):
        """
        Generate a one-use only link for resetting password and send it to the
        user.
        """
        email = self.cleaned_data["email"]
        email_field_name = UserModel.get_email_field_name()
        for user in self.get_users(email):
            if not domain_override:
                current_site = get_current_site(request)
                site_name = current_site.name
                domain = current_site.domain
            else:
                site_name = domain = domain_override
            user_email = getattr(user, email_field_name)
            context = {
                'email': user_email,
                'domain': domain,
                'site_name': site_name,
                'uid': urlsafe_base64_encode(force_bytes(user.pk)),
                'user': user,
                'token': token_generator.make_token(user),
                'protocol': 'https' if use_https else 'http',
                **(extra_email_context or {}),
            }
            self.send_mail(
                subject_template_name, email_template_name, context, from_email,
                user_email, html_email_template_name=html_email_template_name,
            )

comment:3 by Mariusz Felisiak, 4 years ago

Easy pickings: unset
Needs documentation: unset
Resolution: wontfix
Status: newclosed
Note: See TracTickets for help on using tickets.
Back to Top