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
1 change: 1 addition & 0 deletions django/db/models/sql/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -1720,6 +1720,7 @@ def _add_q(
return target_clause, needed_inner

def add_filtered_relation(self, filtered_relation, alias):
self.check_alias(alias)
filtered_relation.alias = alias
relation_lookup_parts, relation_field_parts, _ = self.solve_lookup_type(
filtered_relation.relation_name
Expand Down
42 changes: 21 additions & 21 deletions django/template/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
import inspect
import logging
import re
import warnings
from enum import Enum

from django.template.context import BaseContext
Expand Down Expand Up @@ -88,11 +89,6 @@
# than instantiating SimpleLazyObject with _lazy_re_compile().
tag_re = re.compile(r"({%.*?%}|{{.*?}}|{#.*?#})")

combined_partial_re = re.compile(
r"{%\s*partialdef\s+(?P<name>[\w-]+)(?:\s+inline)?\s*%}"
r"|{%\s*endpartialdef(?:\s+[\w-]+)?\s*%}"
)

logger = logging.getLogger("django.template")


Expand Down Expand Up @@ -300,36 +296,40 @@ class PartialTemplate:
Wraps nodelist as a partial, in order to be able to bind context.
"""

def __init__(self, nodelist, origin, name):
def __init__(self, nodelist, origin, name, source_start=None, source_end=None):
self.nodelist = nodelist
self.origin = origin
self.name = name
# If available (debug mode), the absolute character offsets in the
# template.source correspond to the full partial region.
self._source_start = source_start
self._source_end = source_end

def get_exception_info(self, exception, token):
template = self.origin.loader.get_template(self.origin.template_name)
return template.get_exception_info(exception, token)

def find_partial_source(self, full_source, partial_name):
start_match = None
nesting = 0

for match in combined_partial_re.finditer(full_source):
if name := match["name"]: # Opening tag.
if start_match is None and name == partial_name:
start_match = match
if start_match is not None:
nesting += 1
elif start_match is not None:
nesting -= 1
if nesting == 0:
return full_source[start_match.start() : match.end()]
def find_partial_source(self, full_source):
if (
self._source_start is not None
and self._source_end is not None
and 0 <= self._source_start <= self._source_end <= len(full_source)
):
return full_source[self._source_start : self._source_end]

return ""

@property
def source(self):
template = self.origin.loader.get_template(self.origin.template_name)
return self.find_partial_source(template.source, self.name)
if not template.engine.debug:
warnings.warn(
"PartialTemplate.source is only available when template "
"debugging is enabled.",
RuntimeWarning,
stacklevel=2,
)
return self.find_partial_source(template.source)

def _render(self, context):
return self.nodelist.render(context)
Expand Down
15 changes: 14 additions & 1 deletion django/template/defaulttags.py
Original file line number Diff line number Diff line change
Expand Up @@ -1235,19 +1235,32 @@ def partialdef_func(parser, token):

# Parse the content until the end tag.
valid_endpartials = ("endpartialdef", f"endpartialdef {partial_name}")

pos_open = getattr(token, "position", None)
source_start = pos_open[0] if isinstance(pos_open, tuple) else None

nodelist = parser.parse(valid_endpartials)
endpartial = parser.next_token()
if endpartial.contents not in valid_endpartials:
parser.invalid_block_tag(endpartial, "endpartialdef", valid_endpartials)

pos_close = getattr(endpartial, "position", None)
source_end = pos_close[1] if isinstance(pos_close, tuple) else None

# Store the partial nodelist in the parser.extra_data attribute.
partials = parser.extra_data.setdefault("partials", {})
if partial_name in partials:
raise TemplateSyntaxError(
f"Partial '{partial_name}' is already defined in the "
f"'{parser.origin.name}' template."
)
partials[partial_name] = PartialTemplate(nodelist, parser.origin, partial_name)
partials[partial_name] = PartialTemplate(
nodelist,
parser.origin,
partial_name,
source_start=source_start,
source_end=source_end,
)

return PartialDefNode(partial_name, inline, nodelist)

Expand Down
7 changes: 7 additions & 0 deletions docs/releases/4.2.24.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,10 @@ Django 4.2.24 release notes
*September 3, 2025*

Django 4.2.24 fixes a security issue with severity "high" in 4.2.23.

CVE-2025-57833: Potential SQL injection in ``FilteredRelation`` column aliases
==============================================================================

:class:`.FilteredRelation` was subject to SQL injection in column aliases,
using a suitably crafted dictionary, with dictionary expansion, as the
``**kwargs`` passed to :meth:`.QuerySet.annotate` or :meth:`.QuerySet.alias`.
7 changes: 7 additions & 0 deletions docs/releases/5.1.12.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,10 @@ Django 5.1.12 release notes
*September 3, 2025*

Django 5.1.12 fixes a security issue with severity "high" in 5.1.11.

CVE-2025-57833: Potential SQL injection in ``FilteredRelation`` column aliases
==============================================================================

:class:`.FilteredRelation` was subject to SQL injection in column aliases,
using a suitably crafted dictionary, with dictionary expansion, as the
``**kwargs`` passed to :meth:`.QuerySet.annotate` or :meth:`.QuerySet.alias`.
7 changes: 7 additions & 0 deletions docs/releases/5.2.6.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ Django 5.2.6 release notes

Django 5.2.6 fixes a security issue with severity "high" and one bug in 5.2.5.

CVE-2025-57833: Potential SQL injection in ``FilteredRelation`` column aliases
==============================================================================

:class:`.FilteredRelation` was subject to SQL injection in column aliases,
using a suitably crafted dictionary, with dictionary expansion, as the
``**kwargs`` passed to :meth:`.QuerySet.annotate` or :meth:`.QuerySet.alias`.

Bugfixes
========

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

*Expected October 1, 2025*

Django 5.2.7 fixes several bugs in 5.2.6.

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

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

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

Potential SQL injection in FilteredRelation column aliases.
`Full description
<https://www.djangoproject.com/weblog/2025/sep/03/security-releases/>`__

* Django 5.2 :commit:`(patch) <4c044fcc866ec226f612c475950b690b0139d243>`
* Django 5.1 :commit:`(patch) <102965ea93072fe3c39a30be437c683ec1106ef5>`
* Django 4.2 :commit:`(patch) <31334e6965ad136a5e369993b01721499c5d1a92>`

June 4, 2025 - :cve:`2025-48432`
--------------------------------

Expand Down
34 changes: 34 additions & 0 deletions tests/annotations/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
Exists,
ExpressionWrapper,
F,
FilteredRelation,
FloatField,
Func,
IntegerField,
Expand Down Expand Up @@ -1170,6 +1171,20 @@ def test_alias_sql_injection(self):
with self.assertRaisesMessage(ValueError, msg):
Book.objects.annotate(**{crafted_alias: Value(1)})

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."
# )
msg = (
"Column aliases cannot contain whitespace characters, quotation marks, "
"semicolons, or SQL comments."
)
with self.assertRaisesMessage(ValueError, msg):
Book.objects.annotate(**{crafted_alias: FilteredRelation("author")})

def test_alias_forbidden_chars(self):
tests = [
'al"ias',
Expand Down Expand Up @@ -1202,6 +1217,11 @@ def test_alias_forbidden_chars(self):
with self.assertRaisesMessage(ValueError, msg):
Book.objects.annotate(**{crafted_alias: Value(1)})

with self.assertRaisesMessage(ValueError, msg):
Book.objects.annotate(
**{crafted_alias: FilteredRelation("authors")}
)

def test_alias_containing_percent_sign_deprecation(self):
msg = "Using percent signs in a column alias is deprecated."
with self.assertRaisesMessage(RemovedInDjango70Warning, msg):
Expand Down Expand Up @@ -1505,3 +1525,17 @@ def test_alias_sql_injection(self):
)
with self.assertRaisesMessage(ValueError, msg):
Book.objects.alias(**{crafted_alias: Value(1)})

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."
# )
msg = (
"Column aliases cannot contain whitespace characters, quotation marks, "
"semicolons, or SQL comments."
)
with self.assertRaisesMessage(ValueError, msg):
Book.objects.alias(**{crafted_alias: FilteredRelation("authors")})
Loading
Loading