160 lines
6.5 KiB
Diff
160 lines
6.5 KiB
Diff
|
From fe42da9cdacd9f43fb0d499244314c36f9a11a19 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 2/2] [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 and
|
||
|
Sarah Boyce for the reviews.
|
||
|
---
|
||
|
django/contrib/auth/forms.py | 9 ++++++++-
|
||
|
docs/ref/logging.txt | 12 ++++++++++++
|
||
|
docs/releases/4.2.16.txt | 11 +++++++++++
|
||
|
docs/topics/auth/default.txt | 4 +++-
|
||
|
tests/auth_tests/test_forms.py | 21 +++++++++++++++++++++
|
||
|
tests/mail/custombackend.py | 5 +++++
|
||
|
6 files changed, 60 insertions(+), 2 deletions(-)
|
||
|
|
||
|
diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py
|
||
|
index 061dc81b42..20ce1ba39c 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
|
||
|
@@ -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):
|
||
|
@@ -314,7 +316,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/ref/logging.txt b/docs/ref/logging.txt
|
||
|
index b11fb752f7..3d33e0af63 100644
|
||
|
--- a/docs/ref/logging.txt
|
||
|
+++ b/docs/ref/logging.txt
|
||
|
@@ -204,6 +204,18 @@ all database queries.
|
||
|
Support for logging transaction management queries (``BEGIN``, ``COMMIT``,
|
||
|
and ``ROLLBACK``) was added.
|
||
|
|
||
|
+.. _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-security-logger:
|
||
|
|
||
|
``django.security.*``
|
||
|
diff --git a/docs/releases/4.2.16.txt b/docs/releases/4.2.16.txt
|
||
|
index 043041a97f..4e632d5d77 100644
|
||
|
--- a/docs/releases/4.2.16.txt
|
||
|
+++ b/docs/releases/4.2.16.txt
|
||
|
@@ -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.
|
||
|
diff --git a/docs/topics/auth/default.txt b/docs/topics/auth/default.txt
|
||
|
index 528902416d..ad840c5e57 100644
|
||
|
--- a/docs/topics/auth/default.txt
|
||
|
+++ b/docs/topics/auth/default.txt
|
||
|
@@ -1661,7 +1661,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 81c56a428e..ccb1a26a2b 100644
|
||
|
--- a/tests/auth_tests/test_forms.py
|
||
|
+++ b/tests/auth_tests/test_forms.py
|
||
|
@@ -1245,6 +1245,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"
|
||
|
diff --git a/tests/mail/custombackend.py b/tests/mail/custombackend.py
|
||
|
index 14e7f077ba..c6c567b642 100644
|
||
|
--- a/tests/mail/custombackend.py
|
||
|
+++ b/tests/mail/custombackend.py
|
||
|
@@ -12,3 +12,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.46.0
|
||
|
|