From 58adc28d1a7975cac81228993b6630e435ce44e35bf29fa857c63fa8799032e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mark=C3=A9ta=20Machov=C3=A1?= Date: Mon, 8 Dec 2025 12:08:01 +0100 Subject: [PATCH] CVE-2025-64459 CVE-2025-64460 CVE-2025-13372 --- CVE-2025-13372.patch | 67 ++++++++++++++ CVE-2025-64459.patch | 108 +++++++++++++++++++++++ CVE-2025-64460.patch | 199 ++++++++++++++++++++++++++++++++++++++++++ python-Django.changes | 8 ++ python-Django.spec | 10 ++- 5 files changed, 390 insertions(+), 2 deletions(-) create mode 100644 CVE-2025-13372.patch create mode 100644 CVE-2025-64459.patch create mode 100644 CVE-2025-64460.patch diff --git a/CVE-2025-13372.patch b/CVE-2025-13372.patch new file mode 100644 index 0000000..d06331d --- /dev/null +++ b/CVE-2025-13372.patch @@ -0,0 +1,67 @@ +From 479415ce5249bcdebeb6570c72df2a87f45a7bbf Mon Sep 17 00:00:00 2001 +From: Jacob Walls +Date: Mon, 17 Nov 2025 17:09:54 -0500 +Subject: [PATCH] [5.2.x] 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. + +Backport of 5b90ca1e7591fa36fccf2d6dad67cf1477e6293e from main. +--- + 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 dc2db148aed5..38b61c489838 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 ( + SQLAggregateCompiler, +- SQLCompiler, ++ SQLCompiler as BaseSQLCompiler, + SQLDeleteCompiler, + ) + from django.db.models.sql.compiler import SQLInsertCompiler as BaseSQLInsertCompiler +@@ -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/tests/annotations/tests.py b/tests/annotations/tests.py +index 7a1212122427..78e5408d0fe7 100644 +--- a/tests/annotations/tests.py ++++ b/tests/annotations/tests.py +@@ -1507,3 +1507,14 @@ 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) diff --git a/CVE-2025-64459.patch b/CVE-2025-64459.patch new file mode 100644 index 0000000..1d9d6bc --- /dev/null +++ b/CVE-2025-64459.patch @@ -0,0 +1,108 @@ +From 251f22215061abaa5afddd8c8177cf658c8442e8 Mon Sep 17 00:00:00 2001 +From: Jacob Walls +Date: Wed, 24 Sep 2025 15:54:51 -0400 +Subject: [PATCH 2/3] [5.2.x] Fixed CVE-2025-62769 -- Prevented SQL injections + in Q/QuerySet via the _connector kwarg. + +Thanks cyberstan for the report, Sarah Boyce, Adam Johnson, Simon +Charette, and Jake Howard for the reviews. +--- + django/db/models/query_utils.py | 4 ++++ + docs/releases/4.2.26.txt | 7 +++++++ + docs/releases/5.1.14.txt | 7 +++++++ + docs/releases/5.2.8.txt | 7 +++++++ + tests/queries/test_q.py | 5 +++++ + 5 files changed, 30 insertions(+) + +diff --git a/django/db/models/query_utils.py b/django/db/models/query_utils.py +index d219c5fb0e..9d237cdc77 100644 +--- a/django/db/models/query_utils.py ++++ b/django/db/models/query_utils.py +@@ -48,8 +48,12 @@ class Q(tree.Node): + XOR = "XOR" + default = AND + conditional = True ++ connectors = (None, AND, OR, XOR) + + def __init__(self, *args, _connector=None, _negated=False, **kwargs): ++ if _connector not in self.connectors: ++ connector_reprs = ", ".join(f"{conn!r}" for conn in self.connectors[1:]) ++ raise ValueError(f"_connector must be one of {connector_reprs}, or None.") + super().__init__( + children=[*args, *sorted(kwargs.items())], + connector=_connector, +diff --git a/tests/queries/test_q.py b/tests/queries/test_q.py +index 1a62aca061..52200b2ecf 100644 +--- a/tests/queries/test_q.py ++++ b/tests/queries/test_q.py +@@ -272,6 +272,11 @@ class QTests(SimpleTestCase): + Q(*items, _connector=connector), + ) + ++ def test_connector_validation(self): ++ msg = f"_connector must be one of {Q.AND!r}, {Q.OR!r}, {Q.XOR!r}, or None." ++ with self.assertRaisesMessage(ValueError, msg): ++ Q(_connector="evil") ++ + def test_referenced_base_fields(self): + # Make sure Q.referenced_base_fields retrieves all base fields from + # both filters and F expressions. +-- +2.43.0 + +From b13864f3024abe7afd316081cdf7e67b3b984987 Mon Sep 17 00:00:00 2001 +From: Jacob Walls +Date: Wed, 24 Sep 2025 15:56:03 -0400 +Subject: [PATCH 3/3] [5.2.x] Refs CVE-2025-62769 -- Avoided propagating + invalid arguments to Q on dictionary expansion. + +--- + django/db/models/query.py | 5 +++++ + tests/queries/tests.py | 8 ++++++++ + 2 files changed, 13 insertions(+) + +diff --git a/django/db/models/query.py b/django/db/models/query.py +index 535d91d767..8ed028d140 100644 +--- a/django/db/models/query.py ++++ b/django/db/models/query.py +@@ -42,6 +42,8 @@ MAX_GET_RESULTS = 21 + # The maximum number of items to display in a QuerySet.__repr__ + REPR_OUTPUT_SIZE = 20 + ++PROHIBITED_FILTER_KWARGS = frozenset(["_connector", "_negated"]) ++ + + class BaseIterable: + def __init__( +@@ -1512,6 +1514,9 @@ class QuerySet(AltersData): + return clone + + def _filter_or_exclude_inplace(self, negate, args, kwargs): ++ if invalid_kwargs := PROHIBITED_FILTER_KWARGS.intersection(kwargs): ++ invalid_kwargs_str = ", ".join(f"'{k}'" for k in sorted(invalid_kwargs)) ++ raise TypeError(f"The following kwargs are invalid: {invalid_kwargs_str}") + if negate: + self._query.add_q(~Q(*args, **kwargs)) + else: +diff --git a/tests/queries/tests.py b/tests/queries/tests.py +index ffaabf48a0..c1589669b0 100644 +--- a/tests/queries/tests.py ++++ b/tests/queries/tests.py +@@ -4506,6 +4506,14 @@ class TestInvalidValuesRelation(SimpleTestCase): + Annotation.objects.filter(tag__in=[123, "abc"]) + + ++class TestInvalidFilterArguments(TestCase): ++ def test_filter_rejects_invalid_arguments(self): ++ school = School.objects.create() ++ msg = "The following kwargs are invalid: '_connector', '_negated'" ++ with self.assertRaisesMessage(TypeError, msg): ++ School.objects.filter(pk=school.pk, _negated=True, _connector="evil") ++ ++ + class TestTicket24605(TestCase): + def test_ticket_24605(self): + """ +-- +2.43.0 + diff --git a/CVE-2025-64460.patch b/CVE-2025-64460.patch new file mode 100644 index 0000000..400c331 --- /dev/null +++ b/CVE-2025-64460.patch @@ -0,0 +1,199 @@ +From 99e7d22f55497278d0bcb2e15e72ef532e62a31d Mon Sep 17 00:00:00 2001 +From: Shai Berger +Date: Sat, 11 Oct 2025 21:42:56 +0300 +Subject: [PATCH] [5.2.x] 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> + +Backport of 50efb718b31333051bc2dcb06911b8fa1358c98c from main. +--- + 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 360d5309d853..0fa48acf06e5 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 + +@@ -394,19 +415,25 @@ 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/topics/serialization.txt b/docs/topics/serialization.txt +index 1e573e6e1d53..e9523e2ac133 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) diff --git a/python-Django.changes b/python-Django.changes index 52f6bb5..d17b96b 100644 --- a/python-Django.changes +++ b/python-Django.changes @@ -1,3 +1,11 @@ +------------------------------------------------------------------- +Mon Dec 8 11:04:00 UTC 2025 - Markéta Machová + +- Add security patches: + * CVE-2025-64459.patch (bsc#1252926) + * CVE-2025-13372.patch (bsc#1254437) + * CVE-2025-64460.patch (bsc#1254437) + ------------------------------------------------------------------- Thu Oct 2 10:01:50 UTC 2025 - Markéta Machová diff --git a/python-Django.spec b/python-Django.spec index f3ab34e..f762fe0 100644 --- a/python-Django.spec +++ b/python-Django.spec @@ -35,9 +35,15 @@ Patch0: test_strip_tags.patch # PATCH-FIX-UPSTREAM CVE-2025-57833.patch bsc#1248810 Patch1: CVE-2025-57833.patch # PATCH-FIX-UPSTREAM CVE-2025-59681.patch bsc#1250485 -Patch2: CVE-2025-59681.patch +Patch2: CVE-2025-59681.patch # PATCH-FIX-UPSTREAM CVE-2025-59682.patch bsc#1250487 -Patch3: CVE-2025-59682.patch +Patch3: CVE-2025-59682.patch +# PATCH-FIX-UPSTREAM CVE-2025-64459.patch bsc#1252926 +Patch4: CVE-2025-64459.patch +# PATCH-FIX-UPSTREAM CVE-2025-13372.patch bsc#1254437 +Patch5: CVE-2025-13372.patch +# PATCH-FIX-UPSTREAM CVE-2025-64460.patch bsc#1254437 +Patch6: CVE-2025-64460.patch BuildRequires: %{python_module Jinja2 >= 2.9.2} BuildRequires: %{python_module Pillow >= 6.2.0} BuildRequires: %{python_module PyYAML} -- 2.51.1