CVE-2024-39329
Description
An issue was discovered in Django 5.0 before 5.0.7 and 4.2 before 4.2.14. The django.contrib.auth.backends.ModelBackend.authenticate() method allows remote attackers to enumerate users via a timing attack involving login requests for users with an unusable password.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
CVE-2024-39329 is a low-severity timing attack in Django's authentication backend that allows remote attackers to enumerate valid usernames by measuring response times for users with unusable passwords.
Vulnerability
Description
CVE-2024-39329 affects Django versions 5.0 before 5.0.7 and 4.2 before 4.2.14. The vulnerability resides in the django.contrib.auth.backends.ModelBackend.authenticate() method, which performs authentication for users with unusable passwords (e.g., users whose password is not set or is invalid). When processing login requests, the method’s implementation introduces a detectable timing difference between requests for existing usernames with unusable passwords and requests for non-existent usernames [1][2][3].
Attack
Vector
The attack is a remote timing side-channel. An attacker sends a series of login requests targeting specific usernames and measures the response time. The timing discrepancy allows the attacker to distinguish between a username that exists (even with an unusable password) and one that does not. No authentication or prior access is required, and the attack can be performed over a network [2][3].
Impact
A successful attacker can enumerate valid usernames on a Django application, which can then be used in further attacks such as brute-force password guessing or targeted social engineering. The Django project assigned this issue a severity of “low” as it only leaks existence of users, not passwords or other sensitive data [1][3].
Mitigation
The vulnerability is patched in Django 5.0.7 and 4.2.14. Users are strongly encouraged to upgrade immediately. No workarounds have been provided by the Django team, and the issue does not appear on the CISA Known Exploited Vulnerabilities (KEV) catalog at the time of publication [3][4].
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
DjangoPyPI | >= 5.0, < 5.0.7 | 5.0.7 |
DjangoPyPI | >= 4.2, < 4.2.14 | 4.2.14 |
Affected products
30- Django/Djangodescription
- osv-coords29 versionspkg:apk/chainguard/py3.10-djangopkg:apk/chainguard/py3.10-django-binpkg:apk/chainguard/py3.11-djangopkg:apk/chainguard/py3.11-django-binpkg:apk/chainguard/py3.12-djangopkg:apk/chainguard/py3.12-django-binpkg:apk/chainguard/py3.13-djangopkg:apk/chainguard/py3.13-django-binpkg:apk/chainguard/py3-djangopkg:apk/chainguard/py3-supported-djangopkg:apk/wolfi/py3.10-djangopkg:apk/wolfi/py3.10-django-binpkg:apk/wolfi/py3.11-djangopkg:apk/wolfi/py3.11-django-binpkg:apk/wolfi/py3.12-djangopkg:apk/wolfi/py3.12-django-binpkg:apk/wolfi/py3.13-djangopkg:apk/wolfi/py3.13-django-binpkg:apk/wolfi/py3-djangopkg:apk/wolfi/py3-supported-djangopkg:bitnami/djangopkg:pypi/djangopkg:rpm/opensuse/python-Django4&distro=openSUSE%20Tumbleweedpkg:rpm/opensuse/python-Django6&distro=openSUSE%20Tumbleweedpkg:rpm/opensuse/python-Django&distro=openSUSE%20Leap%2015.5pkg:rpm/opensuse/python-Django&distro=openSUSE%20Leap%2015.6pkg:rpm/opensuse/python-Django&distro=openSUSE%20Tumbleweedpkg:rpm/suse/python-Django&distro=SUSE%20Linux%20Enterprise%20Module%20for%20Package%20Hub%2015%20SP6pkg:rpm/suse/python-Django&distro=SUSE%20Package%20Hub%2015%20SP5
< 5.0.7-r0+ 28 more
- (no CPE)range: < 5.0.7-r0
- (no CPE)range: < 5.0.7-r0
- (no CPE)range: < 5.0.7-r0
- (no CPE)range: < 5.0.7-r0
- (no CPE)range: < 5.0.7-r0
- (no CPE)range: < 5.0.7-r0
- (no CPE)range: < 5.0.7-r0
- (no CPE)range: < 5.0.7-r0
- (no CPE)range: < 5.0.7-r0
- (no CPE)range: < 5.0.7-r0
- (no CPE)range: < 5.0.7-r0
- (no CPE)range: < 5.0.7-r0
- (no CPE)range: < 5.0.7-r0
- (no CPE)range: < 5.0.7-r0
- (no CPE)range: < 5.0.7-r0
- (no CPE)range: < 5.0.7-r0
- (no CPE)range: < 5.0.7-r0
- (no CPE)range: < 5.0.7-r0
- (no CPE)range: < 5.0.7-r0
- (no CPE)range: < 5.0.7-r0
- (no CPE)range: >= 4.2.0, < 4.2.14
- (no CPE)range: >= 5.0, < 5.0.7
- (no CPE)range: < 4.2.14-1.1
- (no CPE)range: < 6.0-1.1
- (no CPE)range: < 2.0.7-150000.1.20.1
- (no CPE)range: < 4.2.11-150600.3.3.1
- (no CPE)range: < 5.0.7-2.1
- (no CPE)range: < 4.2.11-150600.3.3.1
- (no CPE)range: < 2.2.28-bp155.7.12.1
Patches
498cf264c9cb0deec9b933ee8156d3186c96e[4.2.x] Fixed CVE-2024-39329 -- Standarized timing of verify_password() when checking unusuable passwords.
3 files changed · +47 −2
django/contrib/auth/hashers.py+8 −2 modified@@ -43,14 +43,20 @@ def check_password(password, encoded, setter=None, preferred="default"): If setter is specified, it'll be called when you need to regenerate the password. """ - if password is None or not is_password_usable(encoded): - return False + fake_runtime = password is None or not is_password_usable(encoded) preferred = get_hasher(preferred) try: hasher = identify_hasher(encoded) except ValueError: # encoded is gibberish or uses a hasher that's no longer installed. + fake_runtime = True + + if fake_runtime: + # Run the default password hasher once to reduce the timing difference + # between an existing user with an unusable password and a nonexistent + # user or missing hasher (similar to #20760). + make_password(get_random_string(UNUSABLE_PASSWORD_SUFFIX_LENGTH)) return False hasher_changed = hasher.algorithm != preferred.algorithm
docs/releases/4.2.14.txt+7 −0 modified@@ -13,3 +13,10 @@ CVE-2024-38875: Potential denial-of-service vulnerability in ``django.utils.html :tfilter:`urlize` and :tfilter:`urlizetrunc` were subject to a potential denial-of-service attack via certain inputs with a very large number of brackets. + +CVE-2024-39329: Username enumeration through timing difference for users with unusable passwords +================================================================================================ + +The :meth:`~django.contrib.auth.backends.ModelBackend.authenticate()` method +allowed remote attackers to enumerate users via a timing attack involving login +requests for users with unusable passwords.
tests/auth_tests/test_hashers.py+32 −0 modified@@ -613,6 +613,38 @@ def test_check_password_calls_harden_runtime(self): check_password("wrong_password", encoded) self.assertEqual(hasher.harden_runtime.call_count, 1) + def test_check_password_calls_make_password_to_fake_runtime(self): + hasher = get_hasher("default") + cases = [ + (None, None, None), # no plain text password provided + ("foo", make_password(password=None), None), # unusable encoded + ("letmein", make_password(password="letmein"), ValueError), # valid encoded + ] + for password, encoded, hasher_side_effect in cases: + with ( + self.subTest(encoded=encoded), + mock.patch( + "django.contrib.auth.hashers.identify_hasher", + side_effect=hasher_side_effect, + ) as mock_identify_hasher, + mock.patch( + "django.contrib.auth.hashers.make_password" + ) as mock_make_password, + mock.patch( + "django.contrib.auth.hashers.get_random_string", + side_effect=lambda size: "x" * size, + ), + mock.patch.object(hasher, "verify"), + ): + # Ensure make_password is called to standardize timing. + check_password(password, encoded) + self.assertEqual(hasher.verify.call_count, 0) + self.assertEqual(mock_identify_hasher.mock_calls, [mock.call(encoded)]) + self.assertEqual( + mock_make_password.mock_calls, + [mock.call("x" * UNUSABLE_PASSWORD_SUFFIX_LENGTH)], + ) + def test_encode_invalid_salt(self): hasher_classes = [ MD5PasswordHasher,
07cefdee4a9d[5.0.x] Fixed CVE-2024-39329 -- Standarized timing of verify_password() when checking unusuable passwords.
4 files changed · +54 −2
django/contrib/auth/hashers.py+8 −2 modified@@ -40,14 +40,20 @@ def verify_password(password, encoded, preferred="default"): three part encoded digest, and the second whether to regenerate the password. """ - if password is None or not is_password_usable(encoded): - return False, False + fake_runtime = password is None or not is_password_usable(encoded) preferred = get_hasher(preferred) try: hasher = identify_hasher(encoded) except ValueError: # encoded is gibberish or uses a hasher that's no longer installed. + fake_runtime = True + + if fake_runtime: + # Run the default password hasher once to reduce the timing difference + # between an existing user with an unusable password and a nonexistent + # user or missing hasher (similar to #20760). + make_password(get_random_string(UNUSABLE_PASSWORD_SUFFIX_LENGTH)) return False, False hasher_changed = hasher.algorithm != preferred.algorithm
docs/releases/4.2.14.txt+7 −0 modified@@ -13,3 +13,10 @@ CVE-2024-38875: Potential denial-of-service vulnerability in ``django.utils.html :tfilter:`urlize` and :tfilter:`urlizetrunc` were subject to a potential denial-of-service attack via certain inputs with a very large number of brackets. + +CVE-2024-39329: Username enumeration through timing difference for users with unusable passwords +================================================================================================ + +The :meth:`~django.contrib.auth.backends.ModelBackend.authenticate()` method +allowed remote attackers to enumerate users via a timing attack involving login +requests for users with unusable passwords.
docs/releases/5.0.7.txt+7 −0 modified@@ -14,6 +14,13 @@ CVE-2024-38875: Potential denial-of-service vulnerability in ``django.utils.html denial-of-service attack via certain inputs with a very large number of brackets. +CVE-2024-39329: Username enumeration through timing difference for users with unusable passwords +================================================================================================ + +The :meth:`~django.contrib.auth.backends.ModelBackend.authenticate()` method +allowed remote attackers to enumerate users via a timing attack involving login +requests for users with unusable passwords. + Bugfixes ========
tests/auth_tests/test_hashers.py+32 −0 modified@@ -565,6 +565,38 @@ def test_check_password_calls_harden_runtime(self): check_password("wrong_password", encoded) self.assertEqual(hasher.harden_runtime.call_count, 1) + def test_check_password_calls_make_password_to_fake_runtime(self): + hasher = get_hasher("default") + cases = [ + (None, None, None), # no plain text password provided + ("foo", make_password(password=None), None), # unusable encoded + ("letmein", make_password(password="letmein"), ValueError), # valid encoded + ] + for password, encoded, hasher_side_effect in cases: + with ( + self.subTest(encoded=encoded), + mock.patch( + "django.contrib.auth.hashers.identify_hasher", + side_effect=hasher_side_effect, + ) as mock_identify_hasher, + mock.patch( + "django.contrib.auth.hashers.make_password" + ) as mock_make_password, + mock.patch( + "django.contrib.auth.hashers.get_random_string", + side_effect=lambda size: "x" * size, + ), + mock.patch.object(hasher, "verify"), + ): + # Ensure make_password is called to standardize timing. + check_password(password, encoded) + self.assertEqual(hasher.verify.call_count, 0) + self.assertEqual(mock_identify_hasher.mock_calls, [mock.call(encoded)]) + self.assertEqual( + mock_make_password.mock_calls, + [mock.call("x" * UNUSABLE_PASSWORD_SUFFIX_LENGTH)], + ) + def test_encode_invalid_salt(self): hasher_classes = [ MD5PasswordHasher,
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
11- github.com/advisories/GHSA-x7q2-wr7g-xqmfghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-39329ghsaADVISORY
- docs.djangoproject.com/en/dev/releases/securityghsaWEB
- github.com/django/django/commit/07cefdee4a9d1fcd9a3a631cbd07c78defd1923bghsaWEB
- github.com/django/django/commit/156d3186c96e3ec2ca73b8b25dc2ef366e38df14ghsaWEB
- github.com/pypa/advisory-database/tree/main/vulns/django/PYSEC-2024-57.yamlghsaWEB
- groups.google.com/forum/ghsaWEB
- security.netapp.com/advisory/ntap-20240808-0005ghsaWEB
- www.djangoproject.com/weblog/2024/jul/09/security-releasesghsaWEB
- docs.djangoproject.com/en/dev/releases/security/mitre
- www.djangoproject.com/weblog/2024/jul/09/security-releases/mitre
News mentions
0No linked articles in our index yet.