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
39 changes: 33 additions & 6 deletions django/core/serializers/xml_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
"""

import json
from xml.dom import pulldom
from contextlib import contextmanager
from xml.dom import minidom, pulldom
from xml.sax import handler
from xml.sax.expatreader import ExpatParser as _ExpatParser

Expand All @@ -15,6 +16,25 @@
from django.utils.xmlutils import SimplerXMLGenerator, UnserializableContentError


@contextmanager
def fast_cache_clearing():
"""Workaround for performance issues in minidom document checks.

Speeds up repeated DOM operations by skipping unnecessary full traversal
of the DOM tree.
"""
module_helper_was_lambda = False
if original_fn := getattr(minidom, "_in_document", None):
module_helper_was_lambda = original_fn.__name__ == "<lambda>"
if not module_helper_was_lambda:
minidom._in_document = lambda node: bool(node.ownerDocument)
try:
yield
finally:
if original_fn and not module_helper_was_lambda:
minidom._in_document = original_fn


class Serializer(base.Serializer):
"""Serialize a QuerySet to XML."""

Expand Down Expand Up @@ -210,7 +230,8 @@ def _make_parser(self):
def __next__(self):
for event, node in self.event_stream:
if event == "START_ELEMENT" and node.nodeName == "object":
self.event_stream.expandNode(node)
with fast_cache_clearing():
self.event_stream.expandNode(node)
return self._handle_object(node)
raise StopIteration

Expand Down Expand Up @@ -397,20 +418,26 @@ def _get_model_from_node(self, node, attr):

def getInnerText(node):
"""Get all the inner text of a DOM node (recursively)."""
inner_text_list = getInnerTextList(node)
return "".join(inner_text_list)


def getInnerTextList(node):
"""Return a list of the inner texts of a DOM node (recursively)."""
# inspired by
# https://mail.python.org/pipermail/xml-sig/2005-March/011022.html
inner_text = []
result = []
for child in node.childNodes:
if (
child.nodeType == child.TEXT_NODE
or child.nodeType == child.CDATA_SECTION_NODE
):
inner_text.append(child.data)
result.append(child.data)
elif child.nodeType == child.ELEMENT_NODE:
inner_text.extend(getInnerText(child))
result.extend(getInnerTextList(child))
else:
pass
return "".join(inner_text)
return result


# Below code based on Christian Heimes' defusedxml
Expand Down
11 changes: 10 additions & 1 deletion django/db/backends/postgresql/compiler.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from django.db.models.sql.compiler import ( # isort:skip
SQLAggregateCompiler,
SQLCompiler,
SQLCompiler as BaseSQLCompiler,
SQLDeleteCompiler,
SQLInsertCompiler as BaseSQLInsertCompiler,
SQLUpdateCompiler,
Expand All @@ -25,6 +25,15 @@ def __str__(self):
return "UNNEST(%s)" % ", ".join(self)


class SQLCompiler(BaseSQLCompiler):
def quote_name_unless_alias(self, name):
if "$" in name:
raise ValueError(
"Dollar signs are not permitted in column aliases on PostgreSQL."
)
return super().quote_name_unless_alias(name)


class SQLInsertCompiler(BaseSQLInsertCompiler):
def assemble_as_sql(self, fields, value_rows):
# Specialize bulk-insertion of literal values through UNNEST to
Expand Down
18 changes: 18 additions & 0 deletions docs/releases/4.2.27.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,24 @@ Django 4.2.27 release notes
Django 4.2.27 fixes one security issue with severity "high", one security issue
with severity "moderate", and one bug in 4.2.26.

CVE-2025-13372: Potential SQL injection in ``FilteredRelation`` column aliases on PostgreSQL
============================================================================================

: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` on
PostgreSQL.

CVE-2025-64460: Potential denial-of-service vulnerability in XML ``Deserializer``
=================================================================================

:ref:`XML Serialization <serialization-formats-xml>` was subject to a potential
denial-of-service attack due to quadratic time complexity when deserializing
crafted documents containing many nested invalid elements. The internal helper
``django.core.serializers.xml_serializer.getInnerText()`` previously
accumulated inner text inefficiently during recursion. It now collects text per
element, avoiding excessive resource usage.

Bugfixes
========

Expand Down
18 changes: 18 additions & 0 deletions docs/releases/5.1.15.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,24 @@ Django 5.1.15 release notes
Django 5.1.15 fixes one security issue with severity "high", one security issue
with severity "moderate", and one bug in 5.1.14.

CVE-2025-13372: Potential SQL injection in ``FilteredRelation`` column aliases on PostgreSQL
============================================================================================

: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` on
PostgreSQL.

CVE-2025-64460: Potential denial-of-service vulnerability in XML ``Deserializer``
=================================================================================

:ref:`XML Serialization <serialization-formats-xml>` was subject to a potential
denial-of-service attack due to quadratic time complexity when deserializing
crafted documents containing many nested invalid elements. The internal helper
``django.core.serializers.xml_serializer.getInnerText()`` previously
accumulated inner text inefficiently during recursion. It now collects text per
element, avoiding excessive resource usage.

Bugfixes
========

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

*Expected January 6, 2026*

Django 5.2.10 fixes several bugs in 5.2.9.

Bugfixes
========

* ...
18 changes: 18 additions & 0 deletions docs/releases/5.2.9.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,24 @@ Django 5.2.9 release notes
Django 5.2.9 fixes one security issue with severity "high", one security issue
with severity "moderate", and several bugs in 5.2.8.

CVE-2025-13372: Potential SQL injection in ``FilteredRelation`` column aliases on PostgreSQL
============================================================================================

: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` on
PostgreSQL.

CVE-2025-64460: Potential denial-of-service vulnerability in XML ``Deserializer``
=================================================================================

:ref:`XML Serialization <serialization-formats-xml>` was subject to a potential
denial-of-service attack due to quadratic time complexity when deserializing
crafted documents containing many nested invalid elements. The internal helper
``django.core.serializers.xml_serializer.getInnerText()`` previously
accumulated inner text inefficiently during recursion. It now collects text per
element, avoiding excessive resource usage.

Bugfixes
========

Expand Down
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.10
5.2.9
5.2.8
5.2.7
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.

December 2, 2025 - :cve:`2025-13372`
------------------------------------

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

* Django 6.0 :commit:`(patch) <56aea00c3c5e1aacf4ed05f8ee06c2e78f02cea0>`
* Django 5.2 :commit:`(patch) <479415ce5249bcdebeb6570c72df2a87f45a7bbf>`
* Django 5.1 :commit:`(patch) <9c6a5bde24240382807d13bc3748d08444709355>`
* Django 4.2 :commit:`(patch) <f997037b235f6b5c9e7c4a501491ec45f3400f3d>`

December 2, 2025 - :cve:`2025-64460`
------------------------------------

Potential denial-of-service vulnerability in XML serializer text extraction.
`Full description
<https://www.djangoproject.com/weblog/2025/dec/02/security-releases/>`__

* Django 6.0 :commit:`(patch) <1dbd07a608e495a0c229edaaf84d58d8976313b5>`
* Django 5.2 :commit:`(patch) <99e7d22f55497278d0bcb2e15e72ef532e62a31d>`
* Django 5.1 :commit:`(patch) <0db9ea4669312f1f4973e09f4bca06ab9c1ec74b>`
* Django 4.2 :commit:`(patch) <4d2b8803bebcdefd2b76e9e8fc528d5fddea93f0>`

November 5, 2025 - :cve:`2025-64458`
------------------------------------

Expand Down
2 changes: 2 additions & 0 deletions docs/topics/serialization.txt
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,8 @@ Identifier Information
.. _jsonl: https://jsonlines.org/
.. _PyYAML: https://pyyaml.org/

.. _serialization-formats-xml:

XML
---

Expand Down
11 changes: 11 additions & 0 deletions tests/annotations/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -1541,6 +1541,17 @@ def test_alias_filtered_relation_sql_injection(self):
with self.assertRaisesMessage(ValueError, msg):
Book.objects.alias(**{crafted_alias: FilteredRelation("authors")})

def test_alias_filtered_relation_sql_injection_dollar_sign(self):
qs = Book.objects.alias(
**{"crafted_alia$": FilteredRelation("authors")}
).values("name", "crafted_alia$")
if connection.vendor == "postgresql":
msg = "Dollar signs are not permitted in column aliases on PostgreSQL."
with self.assertRaisesMessage(ValueError, msg):
list(qs)
else:
self.assertEqual(qs.first()["name"], self.b1.name)

def test_values_wrong_alias(self):
expected_message = (
"Cannot resolve keyword 'alias_typo' into field. Choices are: %s"
Expand Down
54 changes: 54 additions & 0 deletions tests/serializers/test_deserialization.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import json
import time
import unittest

from django.core.serializers.base import DeserializationError, DeserializedObject
from django.core.serializers.json import Deserializer as JsonDeserializer
from django.core.serializers.jsonl import Deserializer as JsonlDeserializer
from django.core.serializers.python import Deserializer
from django.core.serializers.xml_serializer import Deserializer as XMLDeserializer
from django.db import models
from django.test import SimpleTestCase
from django.test.utils import garbage_collect

from .models import Author

Expand Down Expand Up @@ -133,3 +137,53 @@ def test_yaml_bytes_input(self):

self.assertEqual(first_item.object, self.jane)
self.assertEqual(second_item.object, self.joe)

def test_crafted_xml_performance(self):
"""The time to process invalid inputs is not quadratic."""

def build_crafted_xml(depth, leaf_text_len):
nested_open = "<nested>" * depth
nested_close = "</nested>" * depth
leaf = "x" * leaf_text_len
field_content = f"{nested_open}{leaf}{nested_close}"
return f"""
<django-objects version="1.0">
<object model="contenttypes.contenttype" pk="1">
<field name="app_label">{field_content}</field>
<field name="model">m</field>
</object>
</django-objects>
"""

def deserialize(crafted_xml):
iterator = XMLDeserializer(crafted_xml)
garbage_collect()

start_time = time.perf_counter()
result = list(iterator)
end_time = time.perf_counter()

self.assertEqual(len(result), 1)
self.assertIsInstance(result[0].object, models.Model)
return end_time - start_time

def assertFactor(label, params, factor=2):
factors = []
prev_time = None
for depth, length in params:
crafted_xml = build_crafted_xml(depth, length)
elapsed = deserialize(crafted_xml)
if prev_time is not None:
factors.append(elapsed / prev_time)
prev_time = elapsed

with self.subTest(label):
# Assert based on the average factor to reduce test flakiness.
self.assertLessEqual(sum(factors) / len(factors), factor)

assertFactor(
"varying depth, varying length",
[(50, 2000), (100, 4000), (200, 8000), (400, 16000), (800, 32000)],
2,
)
assertFactor("constant depth, varying length", [(100, 1), (100, 1000)], 2)