VYPR
Moderate severityNVD Advisory· Published Jul 10, 2024· Updated Nov 4, 2025

CVE-2024-39329

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.

PackageAffected versionsPatched versions
DjangoPyPI
>= 5.0, < 5.0.75.0.7
DjangoPyPI
>= 4.2, < 4.2.144.2.14

Affected products

30

Patches

4
156d3186c96e

[4.2.x] Fixed CVE-2024-39329 -- Standarized timing of verify_password() when checking unusuable passwords.

https://github.com/django/djangoMichael ManfreJun 15, 2024via ghsa
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.

https://github.com/django/djangoMichael ManfreJun 15, 2024via ghsa
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

News mentions

0

No linked articles in our index yet.