Fixed CVE-2024-45231 -- Avoided server error on password reset when email sending fails.

On successful submission of a password reset request, an email is sent
to the accounts known to the system. If sending this email fails (due to
email backend misconfiguration, service provider outage, network issues,
etc.), an attacker might exploit this by detecting which password reset
requests succeed and which ones generate a 500 error response.

Thanks to Thibaut Spriet for the report, and to Mariusz Felisiak, Adam
Johnson, and Sarah Boyce for the reviews.
This commit is contained in:
Natalia 2024-08-19 14:47:38 -03:00
parent 320dd27412
commit 8c35a0a903
8 changed files with 83 additions and 2 deletions

View File

@ -1,3 +1,4 @@
import logging
import unicodedata
from django import forms
@ -16,6 +17,7 @@ from django.utils.translation import gettext
from django.utils.translation import gettext_lazy as _
UserModel = get_user_model()
logger = logging.getLogger("django.contrib.auth")
def _unicode_ci_compare(s1, s2):
@ -418,7 +420,12 @@ class PasswordResetForm(forms.Form):
html_email = loader.render_to_string(html_email_template_name, context)
email_message.attach_alternative(html_email, "text/html")
email_message.send()
try:
email_message.send()
except Exception:
logger.exception(
"Failed to send password reset email to %s", context["user"].pk
)
def get_users(self, email):
"""Given an email, return matching user(s) who should receive a reset.

View File

@ -209,6 +209,18 @@ Django development server. This logger generates an ``INFO`` message upon
detecting a modification in a source code file and may produce ``WARNING``
messages during filesystem inspection and event subscription processes.
.. _django-contrib-auth-logger:
``django.contrib.auth``
~~~~~~~~~~~~~~~~~~~~~~~
.. versionadded:: 4.2.16
Log messages related to :doc:`contrib/auth`, particularly ``ERROR`` messages
are generated when a :class:`~django.contrib.auth.forms.PasswordResetForm` is
successfully submitted but the password reset email cannot be delivered due to
a mail sending exception.
.. _django-contrib-gis-logger:
``django.contrib.gis``

View File

@ -13,3 +13,14 @@ CVE-2024-45230: Potential denial-of-service vulnerability in ``django.utils.html
:tfilter:`urlize` and :tfilter:`urlizetrunc` were subject to a potential
denial-of-service attack via very large inputs with a specific sequence of
characters.
CVE-2024-45231: Potential user email enumeration via response status on password reset
======================================================================================
Due to unhandled email sending failures, the
:class:`~django.contrib.auth.forms.PasswordResetForm` class allowed remote
attackers to enumerate user emails by issuing password reset requests and
observing the outcomes.
To mitigate this risk, exceptions occurring during password reset email sending
are now handled and logged using the :ref:`django-contrib-auth-logger` logger.

View File

@ -13,3 +13,14 @@ CVE-2024-45230: Potential denial-of-service vulnerability in ``django.utils.html
:tfilter:`urlize` and :tfilter:`urlizetrunc` were subject to a potential
denial-of-service attack via very large inputs with a specific sequence of
characters.
CVE-2024-45231: Potential user email enumeration via response status on password reset
======================================================================================
Due to unhandled email sending failures, the
:class:`~django.contrib.auth.forms.PasswordResetForm` class allowed remote
attackers to enumerate user emails by issuing password reset requests and
observing the outcomes.
To mitigate this risk, exceptions occurring during password reset email sending
are now handled and logged using the :ref:`django-contrib-auth-logger` logger.

View File

@ -14,6 +14,17 @@ CVE-2024-45230: Potential denial-of-service vulnerability in ``django.utils.html
denial-of-service attack via very large inputs with a specific sequence of
characters.
CVE-2024-45231: Potential user email enumeration via response status on password reset
======================================================================================
Due to unhandled email sending failures, the
:class:`~django.contrib.auth.forms.PasswordResetForm` class allowed remote
attackers to enumerate user emails by issuing password reset requests and
observing the outcomes.
To mitigate this risk, exceptions occurring during password reset email sending
are now handled and logged using the :ref:`django-contrib-auth-logger` logger.
Bugfixes
========

View File

@ -1732,7 +1732,9 @@ provides several built-in forms located in :mod:`django.contrib.auth.forms`:
.. method:: send_mail(subject_template_name, email_template_name, context, from_email, to_email, html_email_template_name=None)
Uses the arguments to send an ``EmailMultiAlternatives``.
Can be overridden to customize how the email is sent to the user.
Can be overridden to customize how the email is sent to the user. If
you choose to override this method, be mindful of handling potential
exceptions raised due to email sending failures.
:param subject_template_name: the template for the subject.
:param email_template_name: the template for the email body.

View File

@ -1357,6 +1357,27 @@ class PasswordResetFormTest(TestDataMixin, TestCase):
)
)
@override_settings(EMAIL_BACKEND="mail.custombackend.FailingEmailBackend")
def test_save_send_email_exceptions_are_catched_and_logged(self):
(user, username, email) = self.create_dummy_user()
form = PasswordResetForm({"email": email})
self.assertTrue(form.is_valid())
with self.assertLogs("django.contrib.auth", level=0) as cm:
form.save()
self.assertEqual(len(mail.outbox), 0)
self.assertEqual(len(cm.output), 1)
errors = cm.output[0].split("\n")
pk = user.pk
self.assertEqual(
errors[0],
f"ERROR:django.contrib.auth:Failed to send password reset email to {pk}",
)
self.assertEqual(
errors[-1], "ValueError: FailingEmailBackend is doomed to fail."
)
@override_settings(AUTH_USER_MODEL="auth_tests.CustomEmailField")
def test_custom_email_field(self):
email = "test@mail.com"

View File

@ -12,3 +12,9 @@ class EmailBackend(BaseEmailBackend):
# Messages are stored in an instance variable for testing.
self.test_outbox.extend(email_messages)
return len(email_messages)
class FailingEmailBackend(BaseEmailBackend):
def send_messages(self, email_messages):
raise ValueError("FailingEmailBackend is doomed to fail.")