Skip to content

Commit

Permalink
feat(account): Prevent account enumeration in password reset
Browse files Browse the repository at this point in the history
  • Loading branch information
pennersr committed Dec 10, 2021
1 parent 5b01b5b commit 7a719b6
Show file tree
Hide file tree
Showing 7 changed files with 71 additions and 5 deletions.
19 changes: 19 additions & 0 deletions ChangeLog.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
0.48.0 (unreleased)
*******************

Note worthy changes
-------------------

- Introduced a new setting `ACCOUNT_PREVENT_ENUMERATION` that controls whether
or not information is revealed about whether or not a user account exists.
**Warning**: this is a work in progress, password reset is covered, yet,
signing up is not.


Backwards incompatible changes
------------------------------

- The newly introduced `ACCOUNT_PREVENT_ENUMERATION` defaults to `True` impacting
the current behavior of the password reset flow.


0.47.0 (2021-12-09)
*******************

Expand Down
4 changes: 4 additions & 0 deletions allauth/account/app_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ def _setting(self, name, dflt):
)
return getter(self.prefix + name, dflt)

@property
def PREVENT_ENUMERATION(self):
return self._setting("PREVENT_ENUMERATION", True)

@property
def DEFAULT_HTTP_PROTOCOL(self):
return self._setting("DEFAULT_HTTP_PROTOCOL", "http").lower()
Expand Down
25 changes: 20 additions & 5 deletions allauth/account/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -524,18 +524,34 @@ def clean_email(self):
email = self.cleaned_data["email"]
email = get_adapter().clean_email(email)
self.users = filter_users_by_email(email, is_active=True)
if not self.users:
if not self.users and not app_settings.PREVENT_ENUMERATION:
raise forms.ValidationError(
_("The e-mail address is not assigned to any user account")
)
return self.cleaned_data["email"]

def save(self, request, **kwargs):
current_site = get_current_site(request)
email = self.cleaned_data["email"]
if not self.users:
self._send_unknown_account_mail(request, email)
else:
self._send_password_reset_mail(request, email, self.users, **kwargs)
return email

def _send_unknown_account_mail(self, request, email):
signup_url = build_absolute_uri(request, reverse("account_signup"))
context = {
"current_site": get_current_site(request),
"email": email,
"request": request,
"signup_url": signup_url,
}
get_adapter(request).send_mail("account/email/unknown_account", email, context)

def _send_password_reset_mail(self, request, email, users, **kwargs):
token_generator = kwargs.get("token_generator", default_token_generator)

for user in self.users:
for user in users:

temp_key = token_generator.make_token(user)

Expand All @@ -551,7 +567,7 @@ def save(self, request, **kwargs):
url = build_absolute_uri(request, path)

context = {
"current_site": current_site,
"current_site": get_current_site(request),
"user": user,
"password_reset_url": url,
"request": request,
Expand All @@ -562,7 +578,6 @@ def save(self, request, **kwargs):
get_adapter(request).send_mail(
"account/email/password_reset_key", email, context
)
return self.cleaned_data["email"]


class ResetPasswordKeyForm(PasswordVerificationMixin, forms.Form):
Expand Down
2 changes: 2 additions & 0 deletions allauth/account/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -1464,6 +1464,7 @@ def test_login_on_confirm_uuid_user(self, mocked_gum, mock_perform_login):
assert mock_perform_login.called


@override_settings(ACCOUNT_PREVENT_ENUMERATION=False)
class TestResetPasswordForm(TestCase):
def test_user_email_not_sent_inactive_user(self):
User = get_user_model()
Expand All @@ -1475,6 +1476,7 @@ def test_user_email_not_sent_inactive_user(self):
self.assertFalse(form.is_valid())


@override_settings(ACCOUNT_PREVENT_ENUMERATION=False)
class TestCVE2019_19844(TestCase):

global_request = RequestFactory().get("/")
Expand Down
12 changes: 12 additions & 0 deletions allauth/templates/account/email/unknown_account_message.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{% extends "account/email/base_message.txt" %}
{% load i18n %}

{% block content %}{% autoescape off %}{% blocktrans %}You are receiving this e-mail because you or someone else has requested a
password for your user account. However, we do not have any record of a user
with email {{ email }} in our database.

This mail can be safely ignored if you did not request a password reset.

If it was you, you can sign up for an account using the link below.{% endblocktrans %}

{{ signup_url }}{% endautoescape %}{% endblock %}
4 changes: 4 additions & 0 deletions allauth/templates/account/email/unknown_account_subject.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{% load i18n %}
{% autoescape off %}
{% blocktrans %}Password Reset E-mail{% endblocktrans %}
{% endautoescape %}
10 changes: 10 additions & 0 deletions docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,16 @@ ACCOUNT_PRESERVE_USERNAME_CASING (=True)
when filter on username. For now, the default is set to ``True`` to maintain
backwards compatibility.

ACCOUNT_PREVENT_ENUMERATION (=True)
Controls whether or not information is revealed about whether or not a user
account exists. For example, by entering random email addresses in the
password reset form you can test whether or not those email addresses are
associated with an account. Enabling this setting prevents that, and an email
is always sent, regardless of whether or not the account exists. Note that
there is a slight usability tax to pay because there is no immediate feedback.
**Warning**: this is a work in progress, password reset is covered, yet,
signing up is not.

ACCOUNT_SESSION_REMEMBER (=None)
Controls the life time of the session. Set to ``None`` to ask the user
("Remember me?"), ``False`` to not remember, and ``True`` to always
Expand Down

0 comments on commit 7a719b6

Please sign in to comment.