# Django User Enumeration via Timing Attack: CVE-2024-39329

> Django authentication vulnerability allows user enumeration through timing differences in password validation.

**URL:** https://www.ciptadusa.com/blog/django-user-enumeration-timing-attack-cve-2024-39329  
**Type:** blog  
**Author:** PT Cipta Dua Saudara  
**Category:** Application Security  
**Published:** 2026-05-30  
**Cover:** https://www.ciptadusa.com/media/defaults/blog-cover.svg  

## Article

# Django User Enumeration via Timing Attack: CVE-2024-39329

## Overview

A security vulnerability was discovered in Django's authentication system that allows remote attackers to enumerate valid usernames through timing attacks. Tracked as CVE-2024-39329, this vulnerability affects Django 5.0 before 5.0.7 and 4.2 before 4.2.14.

## Technical Details

### Vulnerability Description

The vulnerability exists in `django.contrib.auth.backends.ModelBackend.authenticate()` method. When processing login requests for users with unusable passwords, the authentication backend exhibits timing differences that can be measured by attackers.

### Root Cause

Django's authentication system sets "unusable passwords" for users who should not be able to log in via password authentication. The `authenticate()` method processes these users differently, creating measurable timing variations.

### Attack Mechanism

```python
# Simplified representation of the vulnerability
def authenticate(self, request, username=None, password=None):
    try:
        user = User.objects.get(username=username)
        if user.has_usable_password():
            # Path 1: Check password (takes longer)
            if user.check_password(password):
                return user
        else:
            # Path 2: Quick return for unusable password (takes less time)
            return None
    except User.DoesNotExist:
        # Path 3: User doesn't exist (intermediate time)
        return None
```

### Timing Analysis

Attackers can measure response times to determine:
1. **User exists with usable password**: Longer response time
2. **User exists with unusable password**: Shorter response time
3. **User doesn't exist**: Intermediate response time

## Exploitation

### Attack Scenario

1. Attacker sends multiple login requests with different usernames
2. Measures response time for each request
3. Identifies valid usernames based on timing patterns
4. Uses enumerated usernames for targeted attacks

### Tools and Techniques

```python
# Example timing measurement (simplified)
import requests
import time

def measure_login_time(username):
    start = time.time()
    response = requests.post('/login/', data={
        'username': username,
        'password': 'wrong_password'
    })
    end = time.time()
    return end - start

# Analyze timing differences
usernames = ['admin', 'user1', 'nonexistent', 'test']
for username in usernames:
    timing = measure_login_time(username)
    print(f"{username}: {timing:.4f}s")
```

## Impact Assessment

### Severity: Medium

- **Confidentiality**: Medium - Username enumeration
- **Integrity**: None
- **Availability**: None

### Business Impact

- Enables targeted brute-force attacks
- Facilitates credential stuffing
- Aids social engineering attempts
- Violates privacy expectations

## Mitigation Strategies

### Immediate Actions

1. **Update Django** to patched versions:
   - Django 5.0.7 or later
   - Django 4.2.14 or later

2. **Implement rate limiting**:
   ```python
   # Using django-ratelimit
   from ratelimit.decorators import ratelimit
   
   @ratelimit(key='ip', rate='5/m', method=['POST'])
   def login_view(request):
       # Login logic
   ```

### Defense in Depth

```python
# Custom authentication backend with constant-time comparison
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.models import User
import hmac

class SecureModelBackend(ModelBackend):
    def authenticate(self, request, username=None, password=None, **kwargs):
        if username is None:
            username = kwargs.get(User.USERNAME_FIELD)
        
        # Always perform password hash even for non-existent users
        try:
            user = User.objects.get(**{User.USERNAME_FIELD: username})
        except User.DoesNotExist:
            # Run hasher to prevent timing-based user enumeration
            User().set_password(password)
            return None
        
        if user.check_password(password) and self.user_can_authenticate(user):
            return user
        return None
```

### Additional Protections

```python
# settings.py - Security configurations

# Account lockout after failed attempts
ACCOUNT_LOCKOUT_ATTEMPTS = 5
ACCOUNT_LOCKOUT_TIMEOUT = 1800  # 30 minutes

# Session security
SESSION_COOKIE_AGE = 3600  # 1 hour
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True

# CSRF protection
CSRF_COOKIE_SECURE = True
CSRF_COOKIE_HTTPONLY = True
```

## Monitoring and Detection

### Indicators of Compromise

1. **Unusual login patterns**: Multiple failed attempts across different usernames
2. **Timing analysis requests**: Consistent request intervals
3. **Distributed attempts**: Login attempts from multiple IPs

### Monitoring Setup

```python
# Log authentication attempts
import logging

logger = logging.getLogger('django.security')

def log_login_attempt(request, username, success):
    logger.info(f"Login attempt: username={username}, success={success}, "
                f"ip={request.META.get('REMOTE_ADDR')}, "
                f"user_agent={request.META.get('HTTP_USER_AGENT')}")
```

## Best Practices for Authentication Security

### Password Policies

1. **Strong hashing**: Use bcrypt, Argon2, or PBKDF2
2. **Salt uniqueness**: Ensure unique salts per password
3. **Work factor tuning**: Adjust based on server capabilities

### Session Management

```python
# Secure session configuration
SESSION_ENGINE = 'django.contrib.sessions.backends.db'
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = 'Lax'
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
```

### Multi-Factor Authentication

```python
# Consider adding MFA
# Using django-otp or similar packages
INSTALLED_APPS = [
    'django_otp',
    'django_otp.plugins.otp_totp',
    # ...
]

MIDDLEWARE = [
    'django_otp.middleware.OTPMiddleware',
    # ...
]
```

## Conclusion

CVE-2024-39329 demonstrates that even subtle timing differences in authentication systems can be exploited. By updating Django and implementing additional security measures, developers can protect their applications from user enumeration attacks.

## References

- [NVD - CVE-2024-39329](https://nvd.nist.gov/vuln/detail/CVE-2024-39329)
- [Django Security Releases](https://docs.djangoproject.com/en/dev/releases/security/)
- [OWASP - Username Enumeration](https://owasp.org/www-community/attacks/Username_enumeration)

---

*Markdown version of https://www.ciptadusa.com/blog/django-user-enumeration-timing-attack-cve-2024-39329 — generated for AI agents and LLM crawlers.*
