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
17 changes: 9 additions & 8 deletions django/db/models/sql/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,12 @@
__all__ = ["Query", "RawQuery"]

# RemovedInDjango70Warning: When the deprecation ends, replace with:
# Quotation marks ('"`[]), whitespace characters, semicolons, percent signs
# or inline SQL comments are forbidden in column aliases.
# FORBIDDEN_ALIAS_PATTERN = _lazy_re_compile(r"['`\"\]\[;\s]|%|--|/\*|\*/")
# Quotation marks ('"`[]), whitespace characters, semicolons, or inline
# Quotation marks ('"`[]), whitespace characters, semicolons, percent signs,
# hashes, or inline SQL comments are forbidden in column aliases.
# FORBIDDEN_ALIAS_PATTERN = _lazy_re_compile(r"['`\"\]\[;\s]|%|#|--|/\*|\*/")
# Quotation marks ('"`[]), whitespace characters, semicolons, hashes, or inline
# SQL comments are forbidden in column aliases.
FORBIDDEN_ALIAS_PATTERN = _lazy_re_compile(r"['`\"\]\[;\s]|--|/\*|\*/")
FORBIDDEN_ALIAS_PATTERN = _lazy_re_compile(r"['`\"\]\[;\s]|#|--|/\*|\*/")

# Inspired from
# https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS
Expand Down Expand Up @@ -1222,11 +1222,12 @@ def check_alias(self, alias):
)
if FORBIDDEN_ALIAS_PATTERN.search(alias):
raise ValueError(
"Column aliases cannot contain whitespace characters, quotation marks, "
"Column aliases cannot contain whitespace characters, hashes, "
# RemovedInDjango70Warning: When the deprecation ends, replace
# with:
# "semicolons, percent signs, or SQL comments."
"semicolons, or SQL comments."
# "quotation marks, semicolons, percent signs, or SQL "
# "comments."
"quotation marks, semicolons, or SQL comments."
)

def add_annotation(self, annotation, alias, select=True):
Expand Down
6 changes: 5 additions & 1 deletion django/utils/archive.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,11 @@ def has_leading_dir(self, paths):
def target_filename(self, to_path, name):
target_path = os.path.abspath(to_path)
filename = os.path.abspath(os.path.join(target_path, name))
if not filename.startswith(target_path):
try:
if os.path.commonpath([target_path, filename]) != target_path:
raise SuspiciousOperation("Archive contains invalid path: '%s'" % name)
except ValueError:
# Different drives on Windows raises ValueError.
raise SuspiciousOperation("Archive contains invalid path: '%s'" % name)
return filename

Expand Down
17 changes: 16 additions & 1 deletion docs/releases/4.2.25.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,19 @@ Django 4.2.25 release notes
Django 4.2.25 fixes one security issue with severity "high" and one security
issue with severity "low" in 4.2.24.

...
CVE-2025-59681: Potential SQL injection in ``QuerySet.annotate()``, ``alias()``, ``aggregate()``, and ``extra()`` on MySQL and MariaDB
======================================================================================================================================

:meth:`.QuerySet.annotate`, :meth:`~.QuerySet.alias`,
:meth:`~.QuerySet.aggregate`, and :meth:`~.QuerySet.extra` methods were subject
to SQL injection in column aliases, using a suitably crafted dictionary, with
dictionary expansion, as the ``**kwargs`` passed to these methods (follow up to
:cve:`2022-28346`).

CVE-2025-59682: Potential partial directory-traversal via ``archive.extract()``
===============================================================================

The ``django.utils.archive.extract()`` function, used by
:option:`startapp --template` and :option:`startproject --template`, allowed
partial directory-traversal via an archive with file paths sharing a common
prefix with the target directory (follow up to :cve:`2021-3281`).
17 changes: 16 additions & 1 deletion docs/releases/5.1.13.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,19 @@ Django 5.1.13 release notes
Django 5.1.13 fixes one security issue with severity "high" and one security
issue with severity "low" in 5.1.12.

...
CVE-2025-59681: Potential SQL injection in ``QuerySet.annotate()``, ``alias()``, ``aggregate()``, and ``extra()`` on MySQL and MariaDB
======================================================================================================================================

:meth:`.QuerySet.annotate`, :meth:`~.QuerySet.alias`,
:meth:`~.QuerySet.aggregate`, and :meth:`~.QuerySet.extra` methods were subject
to SQL injection in column aliases, using a suitably crafted dictionary, with
dictionary expansion, as the ``**kwargs`` passed to these methods (follow up to
:cve:`2022-28346`).

CVE-2025-59682: Potential partial directory-traversal via ``archive.extract()``
===============================================================================

The ``django.utils.archive.extract()`` function, used by
:option:`startapp --template` and :option:`startproject --template`, allowed
partial directory-traversal via an archive with file paths sharing a common
prefix with the target directory (follow up to :cve:`2021-3281`).
17 changes: 17 additions & 0 deletions docs/releases/5.2.7.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,23 @@ Django 5.2.7 fixes one security issue with severity "high", one security issue
with severity "low", and one bug in 5.2.6. Also, the latest string translations
from Transifex are incorporated.

CVE-2025-59681: Potential SQL injection in ``QuerySet.annotate()``, ``alias()``, ``aggregate()``, and ``extra()`` on MySQL and MariaDB
======================================================================================================================================

:meth:`.QuerySet.annotate`, :meth:`~.QuerySet.alias`,
:meth:`~.QuerySet.aggregate`, and :meth:`~.QuerySet.extra` methods were subject
to SQL injection in column aliases, using a suitably crafted dictionary, with
dictionary expansion, as the ``**kwargs`` passed to these methods (follow up to
:cve:`2022-28346`).

CVE-2025-59682: Potential partial directory-traversal via ``archive.extract()``
===============================================================================

The ``django.utils.archive.extract()`` function, used by
:option:`startapp --template` and :option:`startproject --template`, allowed
partial directory-traversal via an archive with file paths sharing a common
prefix with the target directory (follow up to :cve:`2021-3281`).

Bugfixes
========

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

*Expected November 5, 2025*

Django 5.2.8 fixes several bugs in 5.2.7.

Bugfixes
========

* ...

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

5.2.8
5.2.7
5.2.6
5.2.5
Expand Down
24 changes: 24 additions & 0 deletions docs/releases/security.txt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,30 @@ Issues under Django's security process
All security issues have been handled under versions of Django's security
process. These are listed below.

October 1, 2025 - :cve:`2025-59681`
-----------------------------------

Potential SQL injection in ``QuerySet.annotate()``, ``alias()``, ``aggregate()``, and ``extra()`` on MySQL and MariaDB.
`Full description
<https://www.djangoproject.com/weblog/2025/oct/01/security-releases/>`__

* Django 6.0 :commit:`(patch) <4ceaaee7e04b416fc465e838a6ef43ca0ccffafe>`
* Django 5.2 :commit:`(patch) <52fbae0a4dbbe5faa59827f8f05694a0065cc135>`
* Django 5.1 :commit:`(patch) <01d2d770e22bffe53c7f1e611e2bbca94cb8a2e7>`
* Django 4.2 :commit:`(patch) <38d9ef8c7b5cb6ef51b933e51a20e0e0063f33d5>`

October 1, 2025 - :cve:`2025-59682`
-----------------------------------

Potential partial directory-traversal via ``archive.extract()``.
`Full description
<https://www.djangoproject.com/weblog/2025/oct/01/security-releases/>`__

* Django 6.0 :commit:`(patch) <af067f56c1dd467df4abd0ddd409a700da1f03ba>`
* Django 5.2 :commit:`(patch) <ed8fc39d77465eddbde1191a054ae965f6a8a584>`
* Django 5.1 :commit:`(patch) <74fa85c688a87224637155902bcd738bb9e65e11>`
* Django 4.2 :commit:`(patch) <9504bbaa392c9fe37eee9291f5b4c29eb6037619>`

September 3, 2025 - :cve:`2025-57833`
-------------------------------------

Expand Down
4 changes: 2 additions & 2 deletions tests/aggregation/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -2244,8 +2244,8 @@ def test_exists_none_with_aggregate(self):
def test_alias_sql_injection(self):
crafted_alias = """injected_name" from "aggregation_author"; --"""
msg = (
"Column aliases cannot contain whitespace characters, quotation marks, "
"semicolons, or SQL comments."
"Column aliases cannot contain whitespace characters, hashes, quotation "
"marks, semicolons, or SQL comments."
)
with self.assertRaisesMessage(ValueError, msg):
Author.objects.aggregate(**{crafted_alias: Avg("age")})
Expand Down
43 changes: 22 additions & 21 deletions tests/annotations/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -1161,12 +1161,12 @@ def test_alias_sql_injection(self):
crafted_alias = """injected_name" from "annotations_book"; --"""
# RemovedInDjango70Warning: When the deprecation ends, replace with:
# msg = (
# "Column aliases cannot contain whitespace characters, quotation "
# "marks, semicolons, percent signs, or SQL comments."
# "Column aliases cannot contain whitespace characters, hashes, "
# "quotation marks, semicolons, percent signs, or SQL comments."
# )
msg = (
"Column aliases cannot contain whitespace characters, quotation marks, "
"semicolons, or SQL comments."
"Column aliases cannot contain whitespace characters, hashes, quotation "
"marks, semicolons, or SQL comments."
)
with self.assertRaisesMessage(ValueError, msg):
Book.objects.annotate(**{crafted_alias: Value(1)})
Expand All @@ -1175,12 +1175,12 @@ def test_alias_filtered_relation_sql_injection(self):
crafted_alias = """injected_name" from "annotations_book"; --"""
# RemovedInDjango70Warning: When the deprecation ends, replace with:
# msg = (
# "Column aliases cannot contain whitespace characters, quotation "
# "marks, semicolons, percent signs, or SQL comments."
# "Column aliases cannot contain whitespace characters, hashes, "
# "quotation marks, semicolons, percent signs, or SQL comments."
# )
msg = (
"Column aliases cannot contain whitespace characters, quotation marks, "
"semicolons, or SQL comments."
"Column aliases cannot contain whitespace characters, hashes, quotation "
"marks, semicolons, or SQL comments."
)
with self.assertRaisesMessage(ValueError, msg):
Book.objects.annotate(**{crafted_alias: FilteredRelation("author")})
Expand All @@ -1199,18 +1199,19 @@ def test_alias_forbidden_chars(self):
"alias;",
# RemovedInDjango70Warning: When the deprecation ends, add this:
# "alias%",
# [] are used by MSSQL.
# [] and # are used by MSSQL.
"alias[",
"alias]",
"ali#as",
]
# RemovedInDjango70Warning: When the deprecation ends, replace with:
# msg = (
# "Column aliases cannot contain whitespace characters, quotation "
# "marks, semicolons, percent signs, or SQL comments."
# "Column aliases cannot contain whitespace characters, hashes, "
# "quotation marks, semicolons, percent signs, or SQL comments."
# )
msg = (
"Column aliases cannot contain whitespace characters, quotation marks, "
"semicolons, or SQL comments."
"Column aliases cannot contain whitespace characters, hashes, quotation "
"marks, semicolons, or SQL comments."
)
for crafted_alias in tests:
with self.subTest(crafted_alias):
Expand Down Expand Up @@ -1516,12 +1517,12 @@ def test_alias_sql_injection(self):
crafted_alias = """injected_name" from "annotations_book"; --"""
# RemovedInDjango70Warning: When the deprecation ends, replace with:
# msg = (
# "Column aliases cannot contain whitespace characters, quotation "
# "marks, semicolons, percent signs, or SQL comments."
# "Column aliases cannot contain whitespace characters, hashes, "
# "quotation marks, semicolons, percent signs, or SQL comments."
# )
msg = (
"Column aliases cannot contain whitespace characters, quotation marks, "
"semicolons, or SQL comments."
"Column aliases cannot contain whitespace characters, hashes, quotation "
"marks, semicolons, or SQL comments."
)
with self.assertRaisesMessage(ValueError, msg):
Book.objects.alias(**{crafted_alias: Value(1)})
Expand All @@ -1530,12 +1531,12 @@ def test_alias_filtered_relation_sql_injection(self):
crafted_alias = """injected_name" from "annotations_book"; --"""
# RemovedInDjango70Warning: When the deprecation ends, replace with:
# msg = (
# "Column aliases cannot contain whitespace characters, quotation "
# "marks, semicolons, percent signs, or SQL comments."
# "Column aliases cannot contain whitespace characters, hashes, "
# "quotation marks, semicolons, percent signs, or SQL comments."
# )
msg = (
"Column aliases cannot contain whitespace characters, quotation marks, "
"semicolons, or SQL comments."
"Column aliases cannot contain whitespace characters, hashes, quotation "
"marks, semicolons, or SQL comments."
)
with self.assertRaisesMessage(ValueError, msg):
Book.objects.alias(**{crafted_alias: FilteredRelation("authors")})
Expand Down
8 changes: 4 additions & 4 deletions tests/expressions/test_queryset_values.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ def test_values_expression_containing_percent_sign_deprecation_warns_once(self):
def test_values_expression_alias_sql_injection(self):
crafted_alias = """injected_name" from "expressions_company"; --"""
msg = (
"Column aliases cannot contain whitespace characters, quotation marks, "
"semicolons, or SQL comments."
"Column aliases cannot contain whitespace characters, hashes, quotation "
"marks, semicolons, or SQL comments."
)
with self.assertRaisesMessage(ValueError, msg):
Company.objects.values(**{crafted_alias: F("ceo__salary")})
Expand All @@ -54,8 +54,8 @@ def test_values_expression_alias_sql_injection(self):
def test_values_expression_alias_sql_injection_json_field(self):
crafted_alias = """injected_name" from "expressions_company"; --"""
msg = (
"Column aliases cannot contain whitespace characters, quotation marks, "
"semicolons, or SQL comments."
"Column aliases cannot contain whitespace characters, hashes, quotation "
"marks, semicolons, or SQL comments."
)
with self.assertRaisesMessage(ValueError, msg):
JSONFieldModel.objects.values(f"data__{crafted_alias}")
Expand Down
4 changes: 2 additions & 2 deletions tests/queries/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -1967,8 +1967,8 @@ def test_extra_select_literal_percent_s(self):
def test_extra_select_alias_sql_injection(self):
crafted_alias = """injected_name" from "queries_note"; --"""
msg = (
"Column aliases cannot contain whitespace characters, quotation marks, "
"semicolons, or SQL comments."
"Column aliases cannot contain whitespace characters, hashes, quotation "
"marks, semicolons, or SQL comments."
)
with self.assertRaisesMessage(ValueError, msg):
Note.objects.extra(select={crafted_alias: "1"})
Expand Down
19 changes: 19 additions & 0 deletions tests/utils_tests/test_archive.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import sys
import tempfile
import unittest
import zipfile

from django.core.exceptions import SuspiciousOperation
from django.test import SimpleTestCase
Expand Down Expand Up @@ -94,3 +95,21 @@ def test_extract_function_traversal(self):
with self.subTest(entry), tempfile.TemporaryDirectory() as tmpdir:
with self.assertRaisesMessage(SuspiciousOperation, msg % invalid_path):
archive.extract(os.path.join(archives_dir, entry), tmpdir)

def test_extract_function_traversal_startswith(self):
with tempfile.TemporaryDirectory() as tmpdir:
base = os.path.abspath(tmpdir)
tarfile_handle = tempfile.NamedTemporaryFile(suffix=".zip", delete=False)
tar_path = tarfile_handle.name
tarfile_handle.close()
self.addCleanup(os.remove, tar_path)

malicious_member = os.path.join(base + "abc", "evil.txt")
with zipfile.ZipFile(tar_path, "w") as zf:
zf.writestr(malicious_member, "evil\n")
zf.writestr("test.txt", "data\n")

with self.assertRaisesMessage(
SuspiciousOperation, "Archive contains invalid path"
):
archive.extract(tar_path, base)
Loading