From 4d2b8803bebcdefd2b76e9e8fc528d5fddea93f0 Mon Sep 17 00:00:00 2001 From: Shai Berger Date: Sat, 11 Oct 2025 21:42:56 +0300 Subject: [4.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. --- tests/serializers/test_xml.py | 55 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/serializers/test_xml.py b/tests/serializers/test_xml.py index c9df2f2a5b..03462cfed5 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 = "" * 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 = 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 -- cgit v1.3