From 5b90ca1e7591fa36fccf2d6dad67cf1477e6293e Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Mon, 17 Nov 2025 17:09:54 -0500 Subject: [PATCH 1/4] Fixed CVE-2025-13372 -- Protected FilteredRelation against SQL injection in column aliases on PostgreSQL. Follow-up to CVE-2025-57833. Thanks Stackered for the report, and Simon Charette and Mariusz Felisiak for the reviews. --- django/db/backends/postgresql/compiler.py | 11 ++++++++++- docs/releases/4.2.27.txt | 8 ++++++++ docs/releases/5.1.15.txt | 8 ++++++++ docs/releases/5.2.9.txt | 8 ++++++++ tests/annotations/tests.py | 11 +++++++++++ 5 files changed, 45 insertions(+), 1 deletion(-) 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..e95dc63f74ef 100644 --- a/docs/releases/4.2.27.txt +++ b/docs/releases/4.2.27.txt @@ -7,6 +7,14 @@ 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. + Bugfixes ======== diff --git a/docs/releases/5.1.15.txt b/docs/releases/5.1.15.txt index 2c4e02959031..f55623ea9684 100644 --- a/docs/releases/5.1.15.txt +++ b/docs/releases/5.1.15.txt @@ -7,6 +7,14 @@ 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. + Bugfixes ======== diff --git a/docs/releases/5.2.9.txt b/docs/releases/5.2.9.txt index 9dfcc392a036..08c298999a56 100644 --- a/docs/releases/5.2.9.txt +++ b/docs/releases/5.2.9.txt @@ -7,6 +7,14 @@ 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. + Bugfixes ======== 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" From 50efb718b31333051bc2dcb06911b8fa1358c98c Mon Sep 17 00:00:00 2001 From: Shai Berger Date: Sat, 11 Oct 2025 21:42:56 +0300 Subject: [PATCH 2/4] Fixed CVE-2025-64460 -- Corrected quadratic inner text accumulation in XML serializer. Previously, `getInnerText()` recursively used `list.extend()` on strings, which added each character from child nodes as a separate list element. On deeply nested XML content, this caused the overall deserialization work to grow quadratically with input size, potentially allowing disproportionate CPU consumption for crafted XML. The fix separates collection of inner texts from joining them, so that each subtree is joined only once, reducing the complexity to linear in the size of the input. These changes also include a mitigation for a xml.dom.minidom performance issue. Thanks Seokchan Yoon (https://ch4n3.kr/) for report. Co-authored-by: Jacob Walls Co-authored-by: Natalia <124304+nessita@users.noreply.github.com> --- django/core/serializers/xml_serializer.py | 39 +++++++++++++--- docs/releases/4.2.27.txt | 10 +++++ docs/releases/5.1.15.txt | 10 +++++ docs/releases/5.2.9.txt | 10 +++++ docs/topics/serialization.txt | 2 + tests/serializers/test_deserialization.py | 54 +++++++++++++++++++++++ 6 files changed, 119 insertions(+), 6 deletions(-) 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/docs/releases/4.2.27.txt b/docs/releases/4.2.27.txt index e95dc63f74ef..b843f6a4436e 100644 --- a/docs/releases/4.2.27.txt +++ b/docs/releases/4.2.27.txt @@ -15,6 +15,16 @@ 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 f55623ea9684..63ff22732eeb 100644 --- a/docs/releases/5.1.15.txt +++ b/docs/releases/5.1.15.txt @@ -15,6 +15,16 @@ 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.9.txt b/docs/releases/5.2.9.txt index 08c298999a56..ba235d05c69b 100644 --- a/docs/releases/5.2.9.txt +++ b/docs/releases/5.2.9.txt @@ -15,6 +15,16 @@ 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/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/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) From 8d4ec9949aedc11a258d718689550eea61ae8d4c Mon Sep 17 00:00:00 2001 From: Natalia <124304+nessita@users.noreply.github.com> Date: Tue, 2 Dec 2025 11:11:52 -0300 Subject: [PATCH 3/4] Added stub release notes for 5.2.10. --- docs/releases/5.2.10.txt | 12 ++++++++++++ docs/releases/index.txt | 1 + 2 files changed, 13 insertions(+) create mode 100644 docs/releases/5.2.10.txt 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/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 From d0d596042e958809a13b681d7a184ac7b95e0aa3 Mon Sep 17 00:00:00 2001 From: Natalia <124304+nessita@users.noreply.github.com> Date: Tue, 2 Dec 2025 11:30:11 -0300 Subject: [PATCH 4/4] Added CVE-2025-13372 and CVE-2025-64460 to security archive. --- docs/releases/security.txt | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) 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` ------------------------------------