python3-django: patch CVE-2025-64460

Details: https://nvd.nist.gov/vuln/detail/CVE-2025-64460

Backport the patch that explicitly references this CVE in its
commit message.

Signed-off-by: Gyorgy Sarvari <skandigraun@gmail.com>
Signed-off-by: Anuj Mittal <anuj.mittal@oss.qualcomm.com>
This commit is contained in:
Gyorgy Sarvari 2026-02-07 11:33:53 +01:00 committed by Anuj Mittal
parent ba968dda37
commit 4e29baa804
2 changed files with 200 additions and 0 deletions

View File

@ -0,0 +1,199 @@
From f8fd8a25e04e2b6601fc9cdb69dea41db7b4ff18 Mon Sep 17 00:00:00 2001
From: Shai Berger <shai@platonix.com>
Date: Sat, 11 Oct 2025 21:42:56 +0300
Subject: [PATCH] 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 <jacobtylerwalls@gmail.com>
Co-authored-by: Natalia <124304+nessita@users.noreply.github.com>
Backport of 50efb718b31333051bc2dcb06911b8fa1358c98c from main.
CVE: CVE-2025-64460
Upstream-Status: Backport [https://github.com/django/django/commit/0db9ea4669312f1f4973e09f4bca06ab9c1ec74b]
Signed-off-by: Gyorgy Sarvari <skandigraun@gmail.com>
---
django/core/serializers/xml_serializer.py | 39 +++++++++++++---
docs/topics/serialization.txt | 2 +
tests/serializers/test_xml.py | 55 ++++++++++++++++++++++-
3 files changed, 89 insertions(+), 7 deletions(-)
diff --git a/django/core/serializers/xml_serializer.py b/django/core/serializers/xml_serializer.py
index 16b6977..b2837bc 100644
--- a/django/core/serializers/xml_serializer.py
+++ b/django/core/serializers/xml_serializer.py
@@ -3,7 +3,8 @@ XML serializer.
"""
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.db import DEFAULT_DB_ALIAS, models
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."""
@@ -209,7 +229,8 @@ class Deserializer(base.Deserializer):
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
@@ -393,19 +414,25 @@ class Deserializer(base.Deserializer):
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 0bb5764..dc403ca 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_xml.py b/tests/serializers/test_xml.py
index c9df2f2..03462cf 100644
--- a/tests/serializers/test_xml.py
+++ b/tests/serializers/test_xml.py
@@ -1,7 +1,10 @@
+import gc
+import time
from xml.dom import minidom
from django.core import serializers
-from django.core.serializers.xml_serializer import DTDForbidden
+from django.core.serializers.xml_serializer import Deserializer, DTDForbidden
+from django.db import models
from django.test import TestCase, TransactionTestCase
from .tests import SerializersTestBase, SerializersTransactionTestBase
@@ -90,6 +93,56 @@ class XmlSerializerTestCase(SerializersTestBase, TestCase):
with self.assertRaises(DTDForbidden):
next(serializers.deserialize("xml", xml))
+ 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 = Deserializer(crafted_xml)
+ gc.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)
+
class XmlSerializerTransactionTestCase(
SerializersTransactionTestBase, TransactionTestCase

View File

@ -4,6 +4,7 @@ inherit setuptools3
# Windows-specific DoS via NFKC normalization, not applicable to Linux
CVE_STATUS[CVE-2025-27556] = "not-applicable-platform: Issue only applies on Windows"
SRC_URI += "file://CVE-2025-64460.patch"
SRC_URI[sha256sum] = "29019a5763dbd48da1720d687c3522ef40d1c61be6fb2fad27ed79e9f655bc11"
RDEPENDS:${PN} += "\