티스토리 수익 글 보기

티스토리 수익 글 보기

[6.0.x] Fixed CVE-2025-13473 — Standardized timing of check_password… · django/django@d72cc3b · GitHub
Skip to content
/ django Public

Commit d72cc3b

Browse files
RealOrangeOnejacobtylerwalls
authored andcommitted
[6.0.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 3eb814e from main.
1 parent d64ef24 commit d72cc3b

File tree

5 files changed

+88
7
lines changed

5 files changed

+88
7
lines changed

django/contrib/auth/handlers/modwsgi.py

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,47 @@
44
UserModel = auth.get_user_model()
55

66

7+
def _get_user(username):
8+
"""
9+
Return the UserModel instance for `username`.
10+
11+
If no matching user exists, or if the user is inactive, return None, in
12+
which case the default password hasher is run to mitigate timing attacks.
13+
"""
14+
try:
15+
user = UserModel._default_manager.get_by_natural_key(username)
16+
except UserModel.DoesNotExist:
17+
user = None
18+
else:
19+
if not user.is_active:
20+
user = None
21+
22+
if user is None:
23+
# Run the default password hasher once to reduce the timing difference
24+
# between existing/active and nonexistent/inactive users (#20760).
25+
UserModel().set_password("")
26+
27+
return user
28+
29+
730
def check_password(environ, username, password):
831
"""
932
Authenticate against Django's auth database.
1033
1134
mod_wsgi docs specify None, True, False as return value depending
1235
on whether the user exists and authenticates.
36+
37+
Return None if the user does not exist, return False if the user exists but
38+
password is not correct, and return True otherwise.
39+
1340
"""
1441
# db connection state is managed similarly to the wsgi handler
1542
# as mod_wsgi may call these functions outside of a request/response cycle
1643
db.reset_queries()
1744
try:
18-
try:
19-
user = UserModel._default_manager.get_by_natural_key(username)
20-
except UserModel.DoesNotExist:
21-
return None
22-
if not user.is_active:
23-
return None
24-
return user.check_password(password)
45+
user = _get_user(username)
46+
if user:
47+
return user.check_password(password)
2548
finally:
2649
db.close_old_connections()
2750

docs/releases/4.2.28.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,13 @@ Django 4.2.28 release notes
77
Django 4.2.28 fixes three security issues with severity "high", two security
88
issues with severity "moderate", and one security issue with severity "low" in
99
4.2.27.
10+
11+
CVE-2025-13473: Username enumeration through timing difference in mod_wsgi authentication handler
12+
=================================================================================================
13+
14+
The ``django.contrib.auth.handlers.modwsgi.check_password()`` function for
15+
:doc:`authentication via mod_wsgi</howto/deployment/wsgi/apache-auth>`
16+
allowed remote attackers to enumerate users via a timing attack.
17+
18+
This issue has severity "low" according to the :ref:`Django security policy
19+
<security-disclosure>`.

docs/releases/5.2.11.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,13 @@ Django 5.2.11 release notes
77
Django 5.2.11 fixes three security issues with severity "high", two security
88
issues with severity "moderate", and one security issue with severity "low" in
99
5.2.10.
10+
11+
CVE-2025-13473: Username enumeration through timing difference in mod_wsgi authentication handler
12+
=================================================================================================
13+
14+
The ``django.contrib.auth.handlers.modwsgi.check_password()`` function for
15+
:doc:`authentication via mod_wsgi</howto/deployment/wsgi/apache-auth>`
16+
allowed remote attackers to enumerate users via a timing attack.
17+
18+
This issue has severity "low" according to the :ref:`Django security policy
19+
<security-disclosure>`.

docs/releases/6.0.2.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,16 @@ Django 6.0.2 fixes three security issues with severity "high", two security
88
issues with severity "moderate", one security issue with severity "low", and
99
several bugs in 6.0.1.
1010

11+
CVE-2025-13473: Username enumeration through timing difference in mod_wsgi authentication handler
12+
=================================================================================================
13+
14+
The ``django.contrib.auth.handlers.modwsgi.check_password()`` function for
15+
:doc:`authentication via mod_wsgi</howto/deployment/wsgi/apache-auth>`
16+
allowed remote attackers to enumerate users via a timing attack.
17+
18+
This issue has severity "low" according to the :ref:`Django security policy
19+
<security-disclosure>`.
20+
1121
Bugfixes
1222
========
1323

tests/auth_tests/test_handlers.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
from unittest import mock
2+
13
from django.contrib.auth.handlers.modwsgi import check_password, groups_for_user
4+
from django.contrib.auth.hashers import get_hasher
25
from django.contrib.auth.models import Group, User
36
from django.test import TransactionTestCase, override_settings
47

@@ -73,3 +76,28 @@ def test_groups_for_user(self):
7376

7477
self.assertEqual(groups_for_user({}, "test"), [b"test_group"])
7578
self.assertEqual(groups_for_user({}, "test1"), [])
79+
80+
def test_check_password_fake_runtime(self):
81+
"""
82+
Hasher is run once regardless of whether the user exists. Refs #20760.
83+
"""
84+
User.objects.create_user("test", "test@example.com", "test")
85+
User.objects.create_user("inactive", "test@nono.com", "test", is_active=False)
86+
User.objects.create_user("unusable", "test@nono.com")
87+
88+
hasher = get_hasher()
89+
90+
for username, password in [
91+
("test", "test"),
92+
("test", "wrong"),
93+
("inactive", "test"),
94+
("inactive", "wrong"),
95+
("unusable", "test"),
96+
("doesnotexist", "test"),
97+
]:
98+
with (
99+
self.subTest(username=username, password=password),
100+
mock.patch.object(hasher, "encode") as mock_make_password,
101+
):
102+
check_password({}, username, password)
103+
mock_make_password.assert_called_once()

0 commit comments

Comments
 (0)