From bf4888d317ba4506d091eeac6e8b4f1fcc731199 Mon Sep 17 00:00:00 2001
From: Natalia <124304+nessita@users.noreply.github.com>
Date: Mon, 19 Aug 2024 14:47:38 -0300
Subject: [PATCH] [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.

CVE: CVE-2024-45231

Upstream-Status: Backport [https://github.com/django/django/commit/bf4888d317ba4506d091eeac6e8b4f1fcc731199]

Signed-off-by: Soumya Sambu <soumya.sambu@windriver.com>
---
 django/contrib/auth/forms.py   |  9 ++++++++-
 docs/topics/auth/default.txt   |  4 +++-
 tests/auth_tests/test_forms.py | 21 +++++++++++++++++++++
 tests/mail/custombackend.py    |  5 +++++
 4 files changed, 37 insertions(+), 2 deletions(-)

diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py
index 26d3ca7..dc640a5 100644
--- a/django/contrib/auth/forms.py
+++ b/django/contrib/auth/forms.py
@@ -1,3 +1,4 @@
+import logging
 import unicodedata

 from django import forms
@@ -18,6 +19,7 @@ from django.utils.text import capfirst
 from django.utils.translation import gettext, gettext_lazy as _

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


 def _unicode_ci_compare(s1, s2):
@@ -264,7 +266,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.
diff --git a/docs/topics/auth/default.txt b/docs/topics/auth/default.txt
index 1af4951..6fdf700 100644
--- a/docs/topics/auth/default.txt
+++ b/docs/topics/auth/default.txt
@@ -1530,7 +1530,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.
diff --git a/tests/auth_tests/test_forms.py b/tests/auth_tests/test_forms.py
index e73d4b8..a45fd70 100644
--- a/tests/auth_tests/test_forms.py
+++ b/tests/auth_tests/test_forms.py
@@ -935,6 +935,27 @@ class PasswordResetFormTest(TestDataMixin, TestCase):
             message.get_payload(1).get_payload()
         ))

+    @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'
diff --git a/tests/mail/custombackend.py b/tests/mail/custombackend.py
index fd57777..3e161d1 100644
--- a/tests/mail/custombackend.py
+++ b/tests/mail/custombackend.py
@@ -13,3 +13,8 @@ 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.")
--
2.40.0
