diff --git a/django/core/serializers/xml_serializer.py b/django/core/serializers/xml_serializer.py index 0557af395498..f8ec0865a76b 100644 --- a/django/core/serializers/xml_serializer.py +++ b/django/core/serializers/xml_serializer.py @@ -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 @@ -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__ == "" + 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.""" @@ -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 @@ -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 diff --git a/django/db/backends/postgresql/compiler.py b/django/db/backends/postgresql/compiler.py index 48d0ccfd9d06..08d78e333a5d 100644 --- a/django/db/backends/postgresql/compiler.py +++ b/django/db/backends/postgresql/compiler.py @@ -1,6 +1,6 @@ from django.db.models.sql.compiler import ( # isort:skip SQLAggregateCompiler, - SQLCompiler, + SQLCompiler as BaseSQLCompiler, SQLDeleteCompiler, SQLInsertCompiler as BaseSQLInsertCompiler, SQLUpdateCompiler, @@ -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 diff --git a/docs/releases/4.2.27.txt b/docs/releases/4.2.27.txt index 7ffa5fa45858..b843f6a4436e 100644 --- a/docs/releases/4.2.27.txt +++ b/docs/releases/4.2.27.txt @@ -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 ` 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 ======== diff --git a/docs/releases/5.1.15.txt b/docs/releases/5.1.15.txt index 2c4e02959031..63ff22732eeb 100644 --- a/docs/releases/5.1.15.txt +++ b/docs/releases/5.1.15.txt @@ -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 ` 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 ======== diff --git a/docs/releases/5.2.10.txt b/docs/releases/5.2.10.txt new file mode 100644 index 000000000000..35626cfedc95 --- /dev/null +++ b/docs/releases/5.2.10.txt @@ -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 +======== + +* ... diff --git a/docs/releases/5.2.9.txt b/docs/releases/5.2.9.txt index 9dfcc392a036..ba235d05c69b 100644 --- a/docs/releases/5.2.9.txt +++ b/docs/releases/5.2.9.txt @@ -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 ` 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 ======== diff --git a/docs/releases/index.txt b/docs/releases/index.txt index 1137e7f920d4..9eb42725c77a 100644 --- a/docs/releases/index.txt +++ b/docs/releases/index.txt @@ -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 diff --git a/docs/releases/security.txt b/docs/releases/security.txt index 2c6418afd06f..eacb5dbf2d3b 100644 --- a/docs/releases/security.txt +++ b/docs/releases/security.txt @@ -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 +`__ + +* Django 6.0 :commit:`(patch) <56aea00c3c5e1aacf4ed05f8ee06c2e78f02cea0>` +* Django 5.2 :commit:`(patch) <479415ce5249bcdebeb6570c72df2a87f45a7bbf>` +* Django 5.1 :commit:`(patch) <9c6a5bde24240382807d13bc3748d08444709355>` +* Django 4.2 :commit:`(patch) ` + +December 2, 2025 - :cve:`2025-64460` +------------------------------------ + +Potential denial-of-service vulnerability in XML serializer text extraction. +`Full description +`__ + +* 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` ------------------------------------ diff --git a/docs/topics/serialization.txt b/docs/topics/serialization.txt index f0ac0811be98..2b28f5e15a84 100644 --- a/docs/topics/serialization.txt +++ b/docs/topics/serialization.txt @@ -173,6 +173,8 @@ Identifier Information .. _jsonl: https://jsonlines.org/ .. _PyYAML: https://pyyaml.org/ +.. _serialization-formats-xml: + XML --- diff --git a/tests/annotations/tests.py b/tests/annotations/tests.py index a114480d48e6..10cd05db63f4 100644 --- a/tests/annotations/tests.py +++ b/tests/annotations/tests.py @@ -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" diff --git a/tests/serializers/test_deserialization.py b/tests/serializers/test_deserialization.py index 0bbb46b7ce1c..a718a990385a 100644 --- a/tests/serializers/test_deserialization.py +++ b/tests/serializers/test_deserialization.py @@ -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 @@ -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 = "" * depth + nested_close = "" * depth + leaf = "x" * leaf_text_len + field_content = f"{nested_open}{leaf}{nested_close}" + return f""" + + + {field_content} + m + + + """ + + 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)