FRAMESCAN
← Blog

Django CVE-2026-25673 (URLField DoS) and four other high‑severity issues now detected

FrameScan · 2026-07-22

TL;DR

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

Is your site exposed? Scan it for $1