VYPR
Moderate severityNVD Advisory· Published Dec 2, 2025· Updated Dec 2, 2025

Potential denial-of-service vulnerability in XML serializer text extraction

CVE-2025-64460

Description

An issue was discovered in 5.2 before 5.2.9, 5.1 before 5.1.15, and 4.2 before 4.2.27. Algorithmic complexity in django.core.serializers.xml_serializer.getInnerText() allows a remote attacker to cause a potential denial-of-service attack triggering CPU and memory exhaustion via specially crafted XML input processed by the XML Deserializer. Earlier, unsupported Django series (such as 5.0.x, 4.1.x, and 3.2.x) were not evaluated and may also be affected. Django would like to thank Seokchan Yoon for reporting this issue.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
DjangoPyPI
>= 5.2a1, < 5.2.95.2.9
DjangoPyPI
>= 5.1a1, < 5.1.155.1.15
DjangoPyPI
>= 4.2a1, < 4.2.274.2.27

Affected products

1

Patches

4
4d2b8803bebc

[4.2.x] Fixed CVE-2025-64460 -- Corrected quadratic inner text accumulation in XML serializer.

https://github.com/django/djangoShai BergerOct 11, 2025via ghsa
4 files changed · +99 7
  • django/core/serializers/xml_serializer.py+33 6 modified
    @@ -2,7 +2,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
     
    @@ -14,6 +15,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__ == "<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."""
     
    @@ -208,7 +228,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
     
    @@ -392,19 +413,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
    
  • docs/releases/4.2.27.txt+10 0 modified
    @@ -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 <serialization-formats-xml>` 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
     ========
     
    
  • docs/topics/serialization.txt+2 0 modified
    @@ -173,6 +173,8 @@ Identifier  Information
     .. _jsonl: https://jsonlines.org/
     .. _PyYAML: https://pyyaml.org/
     
    +.. _serialization-formats-xml:
    +
     XML
     ---
     
    
  • tests/serializers/test_xml.py+54 1 modified
    @@ -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 @@ def test_no_dtd(self):
             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
    
1dbd07a608e4

[6.0.x] Fixed CVE-2025-64460 -- Corrected quadratic inner text accumulation in XML serializer.

https://github.com/django/djangoShai BergerOct 11, 2025via ghsa
6 files changed · +119 6
  • django/core/serializers/xml_serializer.py+33 6 modified
    @@ -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__ == "<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."""
     
    @@ -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
    
  • docs/releases/4.2.27.txt+10 0 modified
    @@ -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 <serialization-formats-xml>` 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
     ========
     
    
  • docs/releases/5.1.15.txt+10 0 modified
    @@ -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 <serialization-formats-xml>` 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
     ========
     
    
  • docs/releases/5.2.9.txt+10 0 modified
    @@ -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 <serialization-formats-xml>` 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
     ========
     
    
  • docs/topics/serialization.txt+2 0 modified
    @@ -173,6 +173,8 @@ Identifier  Information
     .. _jsonl: https://jsonlines.org/
     .. _PyYAML: https://pyyaml.org/
     
    +.. _serialization-formats-xml:
    +
     XML
     ---
     
    
  • tests/serializers/test_deserialization.py+54 0 modified
    @@ -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 = "<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 = 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)
    
99e7d22f5549

[5.2.x] Fixed CVE-2025-64460 -- Corrected quadratic inner text accumulation in XML serializer.

https://github.com/django/djangoShai BergerOct 11, 2025via ghsa
6 files changed · +119 6
  • django/core/serializers/xml_serializer.py+33 6 modified
    @@ -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__ == "<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."""
     
    @@ -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
    
  • docs/releases/4.2.27.txt+10 0 modified
    @@ -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 <serialization-formats-xml>` 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
     ========
     
    
  • docs/releases/5.1.15.txt+10 0 modified
    @@ -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 <serialization-formats-xml>` 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
     ========
     
    
  • docs/releases/5.2.9.txt+10 0 modified
    @@ -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 <serialization-formats-xml>` 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
     ========
     
    
  • docs/topics/serialization.txt+2 0 modified
    @@ -173,6 +173,8 @@ Identifier  Information
     .. _jsonl: https://jsonlines.org/
     .. _PyYAML: https://pyyaml.org/
     
    +.. _serialization-formats-xml:
    +
     XML
     ---
     
    
  • tests/serializers/test_deserialization.py+54 0 modified
    @@ -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 = "<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 = 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)
    
0db9ea466931

[5.1.x] Fixed CVE-2025-64460 -- Corrected quadratic inner text accumulation in XML serializer.

https://github.com/django/djangoShai BergerOct 11, 2025via ghsa
5 files changed · +109 7
  • django/core/serializers/xml_serializer.py+33 6 modified
    @@ -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__ == "<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 @@ 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
     
    @@ -393,19 +414,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
    
  • docs/releases/4.2.27.txt+10 0 modified
    @@ -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 <serialization-formats-xml>` 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
     ========
     
    
  • docs/releases/5.1.15.txt+10 0 modified
    @@ -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 <serialization-formats-xml>` 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
     ========
     
    
  • docs/topics/serialization.txt+2 0 modified
    @@ -173,6 +173,8 @@ Identifier  Information
     .. _jsonl: https://jsonlines.org/
     .. _PyYAML: https://pyyaml.org/
     
    +.. _serialization-formats-xml:
    +
     XML
     ---
     
    
  • tests/serializers/test_xml.py+54 1 modified
    @@ -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 @@ def test_no_dtd(self):
             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
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

11

News mentions

0

No linked articles in our index yet.