Skip to content

Commit 96d8404

Browse files
committed
[5.0.x] 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.
1 parent 813de26 commit 96d8404

File tree

7 files changed

+72
-2
lines changed

7 files changed

+72
-2
lines changed

django/contrib/auth/forms.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import logging
12
import unicodedata
23

34
from django import forms
@@ -16,6 +17,7 @@
1617
from django.utils.translation import gettext_lazy as _
1718

1819
UserModel = get_user_model()
20+
logger = logging.getLogger("django.contrib.auth")
1921

2022

2123
def _unicode_ci_compare(s1, s2):
@@ -314,7 +316,12 @@ def send_mail(
314316
html_email = loader.render_to_string(html_email_template_name, context)
315317
email_message.attach_alternative(html_email, "text/html")
316318

317-
email_message.send()
319+
try:
320+
email_message.send()
321+
except Exception:
322+
logger.exception(
323+
"Failed to send password reset email to %s", context["user"].pk
324+
)
318325

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

docs/ref/logging.txt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,18 @@ Django development server. This logger generates an ``INFO`` message upon
214214
detecting a modification in a source code file and may produce ``WARNING``
215215
messages during filesystem inspection and event subscription processes.
216216

217+
.. _django-contrib-auth-logger:
218+
219+
``django.contrib.auth``
220+
~~~~~~~~~~~~~~~~~~~~~~~
221+
222+
.. versionadded:: 4.2.16
223+
224+
Log messages related to :doc:`contrib/auth`, particularly ``ERROR`` messages
225+
are generated when a :class:`~django.contrib.auth.forms.PasswordResetForm` is
226+
successfully submitted but the password reset email cannot be delivered due to
227+
a mail sending exception.
228+
217229
.. _django-contrib-gis-logger:
218230

219231
``django.contrib.gis``

docs/releases/4.2.16.txt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,14 @@ CVE-2024-45230: Potential denial-of-service vulnerability in ``django.utils.html
1313
:tfilter:`urlize` and :tfilter:`urlizetrunc` were subject to a potential
1414
denial-of-service attack via very large inputs with a specific sequence of
1515
characters.
16+
17+
CVE-2024-45231: Potential user email enumeration via response status on password reset
18+
======================================================================================
19+
20+
Due to unhandled email sending failures, the
21+
:class:`~django.contrib.auth.forms.PasswordResetForm` class allowed remote
22+
attackers to enumerate user emails by issuing password reset requests and
23+
observing the outcomes.
24+
25+
To mitigate this risk, exceptions occurring during password reset email sending
26+
are now handled and logged using the :ref:`django-contrib-auth-logger` logger.

docs/releases/5.0.9.txt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,14 @@ CVE-2024-45230: Potential denial-of-service vulnerability in ``django.utils.html
1313
:tfilter:`urlize` and :tfilter:`urlizetrunc` were subject to a potential
1414
denial-of-service attack via very large inputs with a specific sequence of
1515
characters.
16+
17+
CVE-2024-45231: Potential user email enumeration via response status on password reset
18+
======================================================================================
19+
20+
Due to unhandled email sending failures, the
21+
:class:`~django.contrib.auth.forms.PasswordResetForm` class allowed remote
22+
attackers to enumerate user emails by issuing password reset requests and
23+
observing the outcomes.
24+
25+
To mitigate this risk, exceptions occurring during password reset email sending
26+
are now handled and logged using the :ref:`django-contrib-auth-logger` logger.

docs/topics/auth/default.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1685,7 +1685,9 @@ provides several built-in forms located in :mod:`django.contrib.auth.forms`:
16851685
.. method:: send_mail(subject_template_name, email_template_name, context, from_email, to_email, html_email_template_name=None)
16861686

16871687
Uses the arguments to send an ``EmailMultiAlternatives``.
1688-
Can be overridden to customize how the email is sent to the user.
1688+
Can be overridden to customize how the email is sent to the user. If
1689+
you choose to override this method, be mindful of handling potential
1690+
exceptions raised due to email sending failures.
16891691

16901692
:param subject_template_name: the template for the subject.
16911693
:param email_template_name: the template for the email body.

tests/auth_tests/test_forms.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1246,6 +1246,27 @@ def test_save_html_email_template_name(self):
12461246
)
12471247
)
12481248

1249+
@override_settings(EMAIL_BACKEND="mail.custombackend.FailingEmailBackend")
1250+
def test_save_send_email_exceptions_are_catched_and_logged(self):
1251+
(user, username, email) = self.create_dummy_user()
1252+
form = PasswordResetForm({"email": email})
1253+
self.assertTrue(form.is_valid())
1254+
1255+
with self.assertLogs("django.contrib.auth", level=0) as cm:
1256+
form.save()
1257+
1258+
self.assertEqual(len(mail.outbox), 0)
1259+
self.assertEqual(len(cm.output), 1)
1260+
errors = cm.output[0].split("\n")
1261+
pk = user.pk
1262+
self.assertEqual(
1263+
errors[0],
1264+
f"ERROR:django.contrib.auth:Failed to send password reset email to {pk}",
1265+
)
1266+
self.assertEqual(
1267+
errors[-1], "ValueError: FailingEmailBackend is doomed to fail."
1268+
)
1269+
12491270
@override_settings(AUTH_USER_MODEL="auth_tests.CustomEmailField")
12501271
def test_custom_email_field(self):
12511272

tests/mail/custombackend.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,9 @@ def send_messages(self, email_messages):
1212
# Messages are stored in an instance variable for testing.
1313
self.test_outbox.extend(email_messages)
1414
return len(email_messages)
15+
16+
17+
class FailingEmailBackend(BaseEmailBackend):
18+
19+
def send_messages(self, email_messages):
20+
raise ValueError("FailingEmailBackend is doomed to fail.")

0 commit comments

Comments
 (0)