TL;DR
- If your project uses Django 6.0.x, upgrade to 6.0.3 to fix a denial‑of‑service in URL parsing and a race condition in file storage/cache backends.
- If you are on Django 5.2.x, upgrade to 5.2.7 to fix an archive extraction path traversal used by project/app templates.
- If you are on Django 5.1.x, upgrade to 5.1.1 to reduce the risk of user email enumeration in password reset flows.
- Very old Django 0.95 installs should upgrade to 1.0 to avoid a command‑injection issue in message compilation.
- FrameScan already checks for all five CVEs in your next scheduled scan.
At-a-glance
| ID | Impact | Severity | Affected versions | Fixed version |
|---|---|---|---|---|
| CVE-2026-25673 | Remote DoS via crafted URLs | high | django >=6.0,<6.0.3 | 6.0.3 |
| CVE-2026-25674 | Incorrect file perms via race | high | django >=6.0,<6.0.3 | 6.0.3 |
| CVE-2025-59682 | Partial path traversal on extract | high | django >=5.2,<5.2.7 | 5.2.7 |
| CVE-2024-45231 | Email enumeration in reset flow | high | django >=5.1,<5.1.1 | 5.1.1 |
| CVE-2007-0404 | Command injection in tools | high | django >=0.95,<1.0 | 1.0 |
CVE-2026-25673 — URLField.to_python() enables denial of service on Windows
An attacker can send extremely large or specially crafted URLs that trigger slow Unicode normalization during URL parsing, potentially exhausting server resources and degrading availability. If you validate or store URLs from untrusted input on Windows, prioritize this update.
Technical details
The advisory notes that URLField.to_python() calls urllib.parse.urlsplit(), which can perform disproportionately slow NFKC normalization for certain characters on Windows. The Django fix lands in 6.0.3; apps should still bound input size.
Illustrative example — not Django source:
# Validate URL inputs defensively to reduce parsing hotspots
from urllib.parse import urlsplit
MAX_URL_LEN = 2048
def safe_parse_url(candidate: str):
if candidate is None:
return None
data = candidate.strip()
if len(data) > MAX_URL_LEN:
raise ValueError("URL too long")
# Optionally prefilter clearly invalid control chars
if any(ord(ch) < 32 for ch in data):
raise ValueError("Control characters not allowed in URL")
return urlsplit(data, allow_fragments=True)
CVE-2026-25674 — race condition can set wrong file permissions
Concurrent requests can cause temporary umask changes in one thread to leak to others, leading to files or cache entries created with overly permissive or restrictive permissions. This can expose sensitive data or break services. Update promptly on multi‑threaded deployments.
Technical details
The issue affects Django’s file‑system storage and file‑based cache backends. Thread‑local changes to process‑wide umask are racy. Django 6.0.3 addresses this.
Illustrative example — not Django source:
# Avoid changing process-wide umask per request; set explicit modes on create
# and, if unavoidable, serialize umask changes.
import os
import threading
_umask_lock = threading.Lock()
def create_file_atomic(path: str, data: bytes, mode: int = 0o640):
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
# Prefer explicit mode at create time without changing umask
fd = os.open(path, flags, mode)
try:
os.write(fd, data)
finally:
os.close(fd)
def set_umask_safely(new_umask: int):
# If you must change umask, serialize and restore immediately
with _umask_lock:
old = os.umask(new_umask)
try:
# perform work that depends on umask
pass
finally:
os.umask(old)
CVE-2025-59682 — partial directory traversal during archive extraction
A crafted archive used with Django’s template‑based start commands can place files outside the intended directory if file paths share a prefix with the target path. An attacker‑supplied template could overwrite adjacent files. Treat external templates as untrusted and update.
Technical details
The problem is in django.utils.archive.extract(), which needed stricter path validation. Fixed in 5.2.7 (and corresponding patches in other series per the advisory).
Illustrative example — not Django source:
# Safely extract archives by rejecting traversal and prefix tricks
from pathlib import Path
import tarfile
def is_within_directory(base: Path, candidate: Path) -> bool:
try:
base_resolved = base.resolve()
cand_resolved = (base / candidate).resolve()
return str(cand_resolved).startswith(str(base_resolved) + Path.sep)
except FileNotFoundError:
# Resolve even if not yet on disk
return (base / candidate).absolute().as_posix().startswith(base.absolute().as_posix() + "/")
def safe_extract_tar(tar_path: str, dest_dir: str) -> None:
base = Path(dest_dir)
with tarfile.open(tar_path, "r:*") as tf:
for member in tf.getmembers():
member_path = Path(member.name)
if member_path.is_absolute() or ".." in member_path.parts:
raise ValueError("Unsafe path in archive")
if not is_within_directory(base, member_path):
raise ValueError("Path escapes destination directory")
tf.extractall(dest_dir)
CVE-2024-45231 — email enumeration in password reset flows
When email sending repeatedly fails, responses can differ based on whether an address exists, letting an attacker probe for valid user emails. The risk is user privacy exposure and targeted phishing. Update if you use Django’s password reset views.
Technical details
The issue involves django.contrib.auth.forms.PasswordResetForm behavior under consistent email‑send failures. Django 5.1.1 includes the fix.
Illustrative example — not Django source:
# Always return a generic response; never reveal account existence
from django.contrib.auth.forms import PasswordResetForm
from django.core.mail import EmailMessage
from django.http import HttpRequest, HttpResponse
from django.shortcuts import render
GENERIC_MSG = "If an account exists for that email, we sent instructions."
def password_reset_view(request: HttpRequest) -> HttpResponse:
if request.method == "POST":
form = PasswordResetForm(request.POST)
if form.is_valid():
try:
form.save(email_template_name="registration/password_reset_email.html")
except Exception:
# Log internally; response to user stays generic
pass
return render(request, "password_reset_done.html", {"message": GENERIC_MSG})
else:
form = PasswordResetForm()
return render(request, "password_reset_form.html", {"form": form})
CVE-2007-0404 — command injection in message compilation tool
Django 0.95’s bin/compile-messages.py invoked msgfmt via os.system without quoting, allowing shell metacharacters in translation files to execute commands. While very old, this can be dangerous in legacy environments processing untrusted .po/.mo files.
Technical details
The fix is to avoid the shell and pass arguments safely. Upgrade to 1.0.
Illustrative example — not Django source:
# Use subprocess with argument lists instead of os.system
import subprocess
from pathlib import Path
def compile_messages(po_file: str) -> None:
po = Path(po_file)
mo = po.with_suffix(".mo")
subprocess.run(["msgfmt", str(po), "-o", str(mo)], check=True)
Remediation
Upgrade Django to the exact fixed version in your series:
- Django 6.0.x: pip install "django==6.0.3"
- Django 5.2.x: pip install "django==5.2.7"
- Django 5.1.x: pip install "django==5.1.1"
- Django 0.95.x: upgrade at least to 1.0 (pip install "django==1.0")
Review the official advisories for details:
- CVE-2026-25673: https://nvd.nist.gov/vuln/detail/CVE-2026-25673 and https://docs.djangoproject.com/en/dev/releases/security
- CVE-2026-25674: https://nvd.nist.gov/vuln/detail/CVE-2026-25674 and https://docs.djangoproject.com/en/dev/releases/security
- CVE-2025-59682: https://nvd.nist.gov/vuln/detail/CVE-2025-59682
- CVE-2024-45231: https://nvd.nist.gov/vuln/detail/CVE-2024-45231
- CVE-2007-0404: https://nvd.nist.gov/vuln/detail/CVE-2007-0404
Call to action
- Existing customers: see your latest results and rescan here: https://framescan.io/scan
- New to FrameScan? Your first scan per verified domain is free: https://framescan.io/#pricing