Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions django/core/cache/backends/filebased.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from django.core.cache.backends.base import DEFAULT_TIMEOUT, BaseCache
from django.core.files import locks
from django.core.files.move import file_move_safe
from django.utils._os import safe_makedirs


class FileBasedCache(BaseCache):
Expand Down Expand Up @@ -115,13 +116,10 @@ def _cull(self):
self._delete(fname)

def _createdir(self):
# Set the umask because os.makedirs() doesn't apply the "mode" argument
# Workaround because os.makedirs() doesn't apply the "mode" argument
# to intermediate-level directories.
old_umask = os.umask(0o077)
try:
os.makedirs(self._dir, 0o700, exist_ok=True)
finally:
os.umask(old_umask)
# https://github.com/python/cpython/issues/86533
safe_makedirs(self._dir, mode=0o700, exist_ok=True)

def _key_to_file(self, key, version=None):
"""
Expand Down
13 changes: 4 additions & 9 deletions django/core/files/storage/filesystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from django.core.files import File, locks
from django.core.files.move import file_move_safe
from django.core.signals import setting_changed
from django.utils._os import safe_join
from django.utils._os import safe_join, safe_makedirs
from django.utils.deconstruct import deconstructible
from django.utils.encoding import filepath_to_uri
from django.utils.functional import cached_property
Expand Down Expand Up @@ -72,15 +72,10 @@ def _save(self, name, content):
directory = os.path.dirname(full_path)
try:
if self.directory_permissions_mode is not None:
# Set the umask because os.makedirs() doesn't apply the "mode"
# Workaround because os.makedirs() doesn't apply the "mode"
# argument to intermediate-level directories.
old_umask = os.umask(0o777 & ~self.directory_permissions_mode)
try:
os.makedirs(
directory, self.directory_permissions_mode, exist_ok=True
)
finally:
os.umask(old_umask)
# https://github.com/python/cpython/issues/86533
safe_makedirs(directory, self.directory_permissions_mode, exist_ok=True)
else:
os.makedirs(directory, exist_ok=True)
except FileExistsError:
Expand Down
42 changes: 16 additions & 26 deletions django/forms/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
import uuid
from decimal import Decimal, DecimalException
from io import BytesIO
from urllib.parse import urlsplit, urlunsplit

from django.core import validators
from django.core.exceptions import ValidationError
Expand Down Expand Up @@ -780,33 +779,24 @@ def __init__(self, *, assume_scheme=None, **kwargs):
super().__init__(strip=True, **kwargs)

def to_python(self, value):
def split_url(url):
"""
Return a list of url parts via urlsplit(), or raise
ValidationError for some malformed URLs.
"""
try:
return list(urlsplit(url))
except ValueError:
# urlsplit can raise a ValueError with some
# misformatted URLs.
raise ValidationError(self.error_messages["invalid"], code="invalid")

value = super().to_python(value)
if value:
url_fields = split_url(value)
if not url_fields[0]:
# If no URL scheme given, add a scheme.
url_fields[0] = self.assume_scheme
if not url_fields[1]:
# Assume that if no domain is provided, that the path segment
# contains the domain.
url_fields[1] = url_fields[2]
url_fields[2] = ""
# Rebuild the url_fields list, since the domain segment may now
# contain the path too.
url_fields = split_url(urlunsplit(url_fields))
value = urlunsplit(url_fields)
# Detect scheme via partition to avoid calling urlsplit() on
# potentially large or slow-to-normalize inputs.
scheme, sep, _ = value.partition(":")
if (
not sep
or not scheme
or not scheme[0].isascii()
or not scheme[0].isalpha()
or "/" in scheme
):
# No valid scheme found -- prepend the assumed scheme. Handle
# scheme-relative URLs ("//example.com") separately.
if value.startswith("//"):
value = self.assume_scheme + ":" + value
else:
value = self.assume_scheme + "://" + value
return value


Expand Down
58 changes: 57 additions & 1 deletion django/utils/_os.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,67 @@
import os
import tempfile
from os.path import abspath, dirname, join, normcase, sep
from os.path import abspath, curdir, dirname, join, normcase, sep
from pathlib import Path

from django.core.exceptions import SuspiciousFileOperation


# Copied verbatim (minus `os.path` fixes) from:
# https://github.com/python/cpython/pull/23901.
# Python versions >= PY315 may include this fix, so periodic checks are needed
# to remove this vendored copy of `makedirs` once solved upstream.
def makedirs(name, mode=0o777, exist_ok=False, *, parent_mode=None):
"""makedirs(name [, mode=0o777][, exist_ok=False][, parent_mode=None])
Super-mkdir; create a leaf directory and all intermediate ones. Works like
mkdir, except that any intermediate path segment (not just the rightmost)
will be created if it does not exist. If the target directory already
exists, raise an OSError if exist_ok is False. Otherwise no exception is
raised. If parent_mode is not None, it will be used as the mode for any
newly-created, intermediate-level directories. Otherwise, intermediate
directories are created with the default permissions (respecting umask).
This is recursive.
"""
head, tail = os.path.split(name)
if not tail:
head, tail = os.path.split(head)
if head and tail and not os.path.exists(head):
try:
if parent_mode is not None:
makedirs(
head, mode=parent_mode, exist_ok=exist_ok, parent_mode=parent_mode
)
else:
makedirs(head, exist_ok=exist_ok)
except FileExistsError:
# Defeats race condition when another thread created the path
pass
cdir = curdir
if isinstance(tail, bytes):
cdir = bytes(curdir, "ASCII")
if tail == cdir: # xxx/newdir/. exists if xxx/newdir exists
return
try:
os.mkdir(name, mode)
# PY315: The call to `chmod()` is not in the CPython proposed code.
# Apply `chmod()` after `mkdir()` to enforce the exact requested
# permissions, since the kernel masks the mode argument with the
# process umask. This guarantees consistent directory permissions
# without mutating global umask state.
os.chmod(name, mode)
except OSError:
# Cannot rely on checking for EEXIST, since the operating system
# could give priority to other errors like EACCES or EROFS
if not exist_ok or not os.path.isdir(name):
raise


def safe_makedirs(name, mode=0o777, exist_ok=False):
"""Create directories recursively with explicit `mode` on each level."""
makedirs(name=name, mode=mode, exist_ok=exist_ok, parent_mode=mode)


def safe_join(base, *paths):
"""
Join one or more path components to the base path component intelligently.
Expand Down
14 changes: 13 additions & 1 deletion docs/ref/models/querysets.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3275,6 +3275,8 @@ SQL equivalent:

SELECT ... WHERE headline LIKE '%Lennon%';

(The exact SQL syntax varies for each database engine.)

Note this will match the headline ``'Lennon honored today'`` but not ``'lennon
honored today'``.

Expand Down Expand Up @@ -3302,6 +3304,8 @@ SQL equivalent:

SELECT ... WHERE headline ILIKE '%Lennon%';

(The exact SQL syntax varies for each database engine.)

.. admonition:: SQLite users

When using the SQLite backend and non-ASCII strings, bear in mind the
Expand Down Expand Up @@ -3427,6 +3431,8 @@ SQL equivalent:

SELECT ... WHERE headline LIKE 'Lennon%';

(The exact SQL syntax varies for each database engine.)

SQLite doesn't support case-sensitive ``LIKE`` statements; ``startswith`` acts
like ``istartswith`` for SQLite.

Expand All @@ -3447,6 +3453,8 @@ SQL equivalent:

SELECT ... WHERE headline ILIKE 'Lennon%';

(The exact SQL syntax varies for each database engine.)

.. admonition:: SQLite users

When using the SQLite backend and non-ASCII strings, bear in mind the
Expand All @@ -3469,6 +3477,8 @@ SQL equivalent:

SELECT ... WHERE headline LIKE '%Lennon';

(The exact SQL syntax varies for each database engine.)

.. admonition:: SQLite users

SQLite doesn't support case-sensitive ``LIKE`` statements; ``endswith``
Expand All @@ -3490,7 +3500,9 @@ SQL equivalent:

.. code-block:: sql

SELECT ... WHERE headline ILIKE '%Lennon'
SELECT ... WHERE headline ILIKE '%Lennon';

(The exact SQL syntax varies for each database engine.)

.. admonition:: SQLite users

Expand Down
37 changes: 37 additions & 0 deletions docs/releases/4.2.29.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,40 @@ Django 4.2.29 release notes

Django 4.2.29 fixes a security issue with severity "moderate" and a security
issue with severity "low" in 4.2.28.

CVE-2026-25673: Potential denial-of-service vulnerability in ``URLField`` via Unicode normalization on Windows
==============================================================================================================

The :class:`~django.forms.URLField` form field's ``to_python()`` method used
:func:`~urllib.parse.urlsplit` to determine whether to prepend a URL scheme to
the submitted value. On Windows, ``urlsplit()`` performs
:func:`NFKC normalization <python:unicodedata.normalize>`, which can be
disproportionately slow for large inputs containing certain characters.

``URLField.to_python()`` now uses a simplified scheme detection, avoiding
Unicode normalization entirely and deferring URL validation to the appropriate
layers. As a result, while leading and trailing whitespace is still stripped by
default, characters such as newlines, tabs, and other control characters within
the value are no longer handled by ``URLField.to_python()``. When using the
default :class:`~django.core.validators.URLValidator`, these values will
continue to raise :exc:`~django.core.exceptions.ValidationError` during
validation, but if you rely on custom validators, ensure they do not depend on
the previous behavior of ``URLField.to_python()``.

This issue has severity "moderate" according to the :ref:`Django security
policy <security-disclosure>`.

CVE-2026-25674: Potential incorrect permissions on newly created file system objects
====================================================================================

Django's file-system storage and file-based cache backends used the process
``umask`` to control permissions when creating directories. In multi-threaded
environments, one thread's temporary umask change can affect other threads'
file and directory creation, resulting in file system objects being created
with unintended permissions.

Django now applies the requested permissions via :func:`~os.chmod` after
:func:`~os.mkdir`, removing the dependency on the process-wide umask.

This issue has severity "low" according to the :ref:`Django security policy
<security-disclosure>`.
37 changes: 37 additions & 0 deletions docs/releases/5.2.12.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,43 @@ Django 5.2.12 fixes a security issue with severity "moderate" and a security
issue with severity "low" in 5.2.11. It also fixes one bug related to support
for Python 3.14.

CVE-2026-25673: Potential denial-of-service vulnerability in ``URLField`` via Unicode normalization on Windows
==============================================================================================================

The :class:`~django.forms.URLField` form field's ``to_python()`` method used
:func:`~urllib.parse.urlsplit` to determine whether to prepend a URL scheme to
the submitted value. On Windows, ``urlsplit()`` performs
:func:`NFKC normalization <python:unicodedata.normalize>`, which can be
disproportionately slow for large inputs containing certain characters.

``URLField.to_python()`` now uses a simplified scheme detection, avoiding
Unicode normalization entirely and deferring URL validation to the appropriate
layers. As a result, while leading and trailing whitespace is still stripped by
default, characters such as newlines, tabs, and other control characters within
the value are no longer handled by ``URLField.to_python()``. When using the
default :class:`~django.core.validators.URLValidator`, these values will
continue to raise :exc:`~django.core.exceptions.ValidationError` during
validation, but if you rely on custom validators, ensure they do not depend on
the previous behavior of ``URLField.to_python()``.

This issue has severity "moderate" according to the :ref:`Django security
policy <security-disclosure>`.

CVE-2026-25674: Potential incorrect permissions on newly created file system objects
====================================================================================

Django's file-system storage and file-based cache backends used the process
``umask`` to control permissions when creating directories. In multi-threaded
environments, one thread's temporary umask change can affect other threads'
file and directory creation, resulting in file system objects being created
with unintended permissions.

Django now applies the requested permissions via :func:`~os.chmod` after
:func:`~os.mkdir`, removing the dependency on the process-wide umask.

This issue has severity "low" according to the :ref:`Django security policy
<security-disclosure>`.

Bugfixes
========

Expand Down
37 changes: 37 additions & 0 deletions docs/releases/6.0.3.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,43 @@ Django 6.0.3 release notes
Django 6.0.3 fixes a security issue with severity "moderate", a security issue
with severity "low", and several bugs in 6.0.2.

CVE-2026-25673: Potential denial-of-service vulnerability in ``URLField`` via Unicode normalization on Windows
==============================================================================================================

The :class:`~django.forms.URLField` form field's ``to_python()`` method used
:func:`~urllib.parse.urlsplit` to determine whether to prepend a URL scheme to
the submitted value. On Windows, ``urlsplit()`` performs
:func:`NFKC normalization <python:unicodedata.normalize>`, which can be
disproportionately slow for large inputs containing certain characters.

``URLField.to_python()`` now uses a simplified scheme detection, avoiding
Unicode normalization entirely and deferring URL validation to the appropriate
layers. As a result, while leading and trailing whitespace is still stripped by
default, characters such as newlines, tabs, and other control characters within
the value are no longer handled by ``URLField.to_python()``. When using the
default :class:`~django.core.validators.URLValidator`, these values will
continue to raise :exc:`~django.core.exceptions.ValidationError` during
validation, but if you rely on custom validators, ensure they do not depend on
the previous behavior of ``URLField.to_python()``.

This issue has severity "moderate" according to the :ref:`Django security
policy <security-disclosure>`.

CVE-2026-25674: Potential incorrect permissions on newly created file system objects
====================================================================================

Django's file-system storage and file-based cache backends used the process
``umask`` to control permissions when creating directories. In multi-threaded
environments, one thread's temporary umask change can affect other threads'
file and directory creation, resulting in file system objects being created
with unintended permissions.

Django now applies the requested permissions via :func:`~os.chmod` after
:func:`~os.mkdir`, removing the dependency on the process-wide umask.

This issue has severity "low" according to the :ref:`Django security policy
<security-disclosure>`.

Bugfixes
========

Expand Down
12 changes: 12 additions & 0 deletions docs/releases/6.0.4.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
==========================
Django 6.0.4 release notes
==========================

*Expected April 7, 2026*

Django 6.0.4 fixes several bugs in 6.0.3.

Bugfixes
========

* ...
1 change: 1 addition & 0 deletions docs/releases/index.txt
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ versions of the documentation contain the release notes for any later releases.
.. toctree::
:maxdepth: 1

6.0.4
6.0.3
6.0.2
6.0.1
Expand Down
Loading