Skip to content

Commit bf4888d

Browse files
committed
[4.2.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 d147a8e commit bf4888d

File tree

6 files changed

+60
-2
lines changed

6 files changed

+60
-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
@@ -204,6 +204,18 @@ all database queries.
204204
Support for logging transaction management queries (``BEGIN``, ``COMMIT``,
205205
and ``ROLLBACK``) was added.
206206

207+
.. _django-contrib-auth-logger:
208+
209+
``django.contrib.auth``
210+
~~~~~~~~~~~~~~~~~~~~~~~
211+
212+
.. versionadded:: 4.2.16
213+
214+
Log messages related to :doc:`contrib/auth`, particularly ``ERROR`` messages
215+
are generated when a :class:`~django.contrib.auth.forms.PasswordResetForm` is
216+
successfully submitted but the password reset email cannot be delivered due to
217+
a mail sending exception.
218+
207219
.. _django-security-logger:
208220

209221
``django.security.*``

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/topics/auth/default.txt

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

16631663
Uses the arguments to send an ``EmailMultiAlternatives``.
1664-
Can be overridden to customize how the email is sent to the user.
1664+
Can be overridden to customize how the email is sent to the user. If
1665+
you choose to override this method, be mindful of handling potential
1666+
exceptions raised due to email sending failures.
16651667

16661668
:param subject_template_name: the template for the subject.
16671669
: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
@@ -1245,6 +1245,27 @@ def test_save_html_email_template_name(self):
12451245
)
12461246
)
12471247

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

tests/mail/custombackend.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,8 @@ 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+
def send_messages(self, email_messages):
19+
raise ValueError("FailingEmailBackend is doomed to fail.")

0 commit comments

Comments
 (0)