File CVE-2025-13473.patch of Package python-Django.42494

From a48a33a8612866f898449d66be3fb3574df11ce7 Mon Sep 17 00:00:00 2001
From: Jake Howard <git@theorangeone.net>
Date: Wed, 19 Nov 2025 16:52:28 +0000
Subject: [PATCH 1/7] [4.2.x] Fixed CVE-2025-13473 -- Standardized timing of
 check_password() in mod_wsgi auth handler.

Refs CVE-2024-39329, #20760.

Thanks Stackered for the report, and Jacob Walls and Markus Holtermann
for the reviews.

Co-authored-by: Natalia <124304+nessita@users.noreply.github.com>

Backport of 4d63f7545bca3c2370f2c9895f77a88183e99c93 from main.
---
 django/contrib/auth/handlers/modwsgi.py | 37 ++++++++++++++++++++-----
 docs/releases/4.2.28.txt                | 10 +++++++
 tests/auth_tests/test_handlers.py       | 28 +++++++++++++++++++
 3 files changed, 68 insertions(+), 7 deletions(-)

diff --git a/django/contrib/auth/handlers/modwsgi.py b/django/contrib/auth/handlers/modwsgi.py
index 591ec72cb4..086db89fc8 100644
--- a/django/contrib/auth/handlers/modwsgi.py
+++ b/django/contrib/auth/handlers/modwsgi.py
@@ -4,24 +4,47 @@ from django.contrib import auth
 UserModel = auth.get_user_model()
 
 
+def _get_user(username):
+    """
+    Return the UserModel instance for `username`.
+
+    If no matching user exists, or if the user is inactive, return None, in
+    which case the default password hasher is run to mitigate timing attacks.
+    """
+    try:
+        user = UserModel._default_manager.get_by_natural_key(username)
+    except UserModel.DoesNotExist:
+        user = None
+    else:
+        if not user.is_active:
+            user = None
+
+    if user is None:
+        # Run the default password hasher once to reduce the timing difference
+        # between existing/active and nonexistent/inactive users (#20760).
+        UserModel().set_password("")
+
+    return user
+
+
 def check_password(environ, username, password):
     """
     Authenticate against Django's auth database.
 
     mod_wsgi docs specify None, True, False as return value depending
     on whether the user exists and authenticates.
+
+    Return None if the user does not exist, return False if the user exists but
+    password is not correct, and return True otherwise.
+
     """
     # db connection state is managed similarly to the wsgi handler
     # as mod_wsgi may call these functions outside of a request/response cycle
     db.reset_queries()
     try:
-        try:
-            user = UserModel._default_manager.get_by_natural_key(username)
-        except UserModel.DoesNotExist:
-            return None
-        if not user.is_active:
-            return None
-        return user.check_password(password)
+        user = _get_user(username)
+        if user:
+            return user.check_password(password)
     finally:
         db.close_old_connections()
 
diff --git a/tests/auth_tests/test_handlers.py b/tests/auth_tests/test_handlers.py
index a6b53a9ef1..32b4371198 100644
--- a/tests/auth_tests/test_handlers.py
+++ b/tests/auth_tests/test_handlers.py
@@ -1,4 +1,7 @@
+from unittest import mock
+
 from django.contrib.auth.handlers.modwsgi import check_password, groups_for_user
+from django.contrib.auth.hashers import get_hasher
 from django.contrib.auth.models import Group, User
 from django.test import TransactionTestCase, override_settings
 
@@ -73,3 +76,28 @@ class ModWsgiHandlerTestCase(TransactionTestCase):
 
         self.assertEqual(groups_for_user({}, "test"), [b"test_group"])
         self.assertEqual(groups_for_user({}, "test1"), [])
+
+    def test_check_password_fake_runtime(self):
+        """
+        Hasher is run once regardless of whether the user exists. Refs #20760.
+        """
+        User.objects.create_user("test", "test@example.com", "test")
+        User.objects.create_user("inactive", "test@nono.com", "test", is_active=False)
+        User.objects.create_user("unusable", "test@nono.com")
+
+        hasher = get_hasher()
+
+        for username, password in [
+            ("test", "test"),
+            ("test", "wrong"),
+            ("inactive", "test"),
+            ("inactive", "wrong"),
+            ("unusable", "test"),
+            ("doesnotexist", "test"),
+        ]:
+            with (
+                self.subTest(username=username, password=password),
+                mock.patch.object(hasher, "encode") as mock_make_password,
+            ):
+                check_password({}, username, password)
+                mock_make_password.assert_called_once()
-- 
2.50.1 (Apple Git-155)

openSUSE Build Service is sponsored by