Skip to content

Commit 73c5e94

Browse files
Refs #36769 -- Raised SuspiciousOperation for unexpected nested tags in XML Deserializer.
Thanks Shai Berger and Natalia Bidart for reviews.
1 parent a25158f commit 73c5e94

7 files changed

Lines changed: 60 additions & 60 deletions

File tree

django/core/serializers/xml_serializer.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
from django.apps import apps
1212
from django.conf import settings
13-
from django.core.exceptions import ObjectDoesNotExist
13+
from django.core.exceptions import ObjectDoesNotExist, SuspiciousOperation
1414
from django.core.serializers import base
1515
from django.db import DEFAULT_DB_ALIAS, models
1616
from django.utils.xmlutils import SimplerXMLGenerator, UnserializableContentError
@@ -411,6 +411,8 @@ def m2m_convert(n):
411411
try:
412412
for c in node.getElementsByTagName("object"):
413413
values.append(m2m_convert(c))
414+
except SuspiciousOperation:
415+
raise
414416
except Exception as e:
415417
if isinstance(e, ObjectDoesNotExist) and self.handle_forward_references:
416418
return base.DEFER_FIELD
@@ -440,6 +442,8 @@ def _get_model_from_node(self, node, attr):
440442

441443

442444
def check_element_type(element):
445+
if element.childNodes:
446+
raise SuspiciousOperation(f"Unexpected element: {element.tagName!r}")
443447
return element.nodeType in (element.TEXT_NODE, element.CDATA_SECTION_NODE)
444448

445449

docs/releases/6.1.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,10 @@ Serialization
303303
``()``. This ensures primary keys are serialized when using
304304
:option:`dumpdata --natural-primary`.
305305

306+
* The XML deserializer now raises
307+
:exc:`~django.core.exceptions.SuspiciousOperation` when it encounters
308+
unexpected nested tags.
309+
306310
Signals
307311
~~~~~~~
308312

docs/topics/serialization.txt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,8 @@ lowercase name of the model ("session") separated by a dot.
198198

199199
Each field of the object is serialized as a ``<field>``-element sporting the
200200
fields "type" and "name". The text content of the element represents the value
201-
that should be stored.
201+
that should be stored. (If the element contains child tags,
202+
:exc:`~django.core.exceptions.SuspiciousOperation` is raised.)
202203

203204
Foreign keys and other relational fields are treated a little bit differently:
204205

@@ -237,6 +238,11 @@ This example links the given user with the permission models with PKs 46 and
237238
XHTML, XML and Control Codes
238239
<https://www.w3.org/International/questions/qa-controls>`_.
239240

241+
.. versionchanged:: 6.1
242+
243+
:exc:`~django.core.exceptions.SuspiciousOperation` is raised when
244+
unexpected nested tags are found.
245+
240246
.. _serialization-formats-json:
241247

242248
JSON

tests/fixtures/fixtures/invalid_deeply_nested_elements.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
<object pk="1" model="fixtures.person">
55
<field type="CharField" name="name">
6+
<!-- This <em> is unexpected & invalid -->
67
Django <em>pony</em>
78
</field>
89
</object>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<django-objects version="1.0">
3+
4+
<object pk="1" model="fixtures.book">
5+
<field type="CharField" name="name">Music for all ages</field>
6+
<field to="fixtures.person" name="authors" rel="ManyToManyRel">
7+
<object>
8+
<!-- This <em> is unexpected & invalid -->
9+
<natural>Artist formerly known as <em>Prince</em></natural>
10+
</object>
11+
</field>
12+
</object>
13+
14+
</django-objects>

tests/fixtures/tests.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from django.apps import apps
1111
from django.contrib.sites.models import Site
1212
from django.core import management
13+
from django.core.exceptions import SuspiciousOperation
1314
from django.core.files.temp import NamedTemporaryFile
1415
from django.core.management import CommandError
1516
from django.core.management.commands.dumpdata import ProxyModelWarning
@@ -23,7 +24,6 @@
2324
CircularA,
2425
CircularB,
2526
NaturalKeyThing,
26-
Person,
2727
PrimaryKeyUUIDModel,
2828
ProxySpy,
2929
Spy,
@@ -522,12 +522,13 @@ def test_loading_and_dumping(self):
522522
)
523523

524524
def test_deeply_nested_elements(self):
525-
"""Text inside deeply-nested tags is skipped."""
526-
management.call_command(
527-
"loaddata", "invalid_deeply_nested_elements.xml", verbosity=0
528-
)
529-
person = Person.objects.get(pk=1)
530-
self.assertEqual(person.name, "Django") # not "Django pony"
525+
"""Text inside deeply-nested tags raises SuspiciousOperation."""
526+
for file in [
527+
"invalid_deeply_nested_elements.xml",
528+
"invalid_deeply_nested_elements_natural_key.xml",
529+
]:
530+
with self.subTest(file=file), self.assertRaises(SuspiciousOperation):
531+
management.call_command("loaddata", file, verbosity=0)
531532

532533
def test_dumpdata_with_excludes(self):
533534
# Load fixture1 which has a site, two articles, and a category

tests/serializers/test_deserialization.py

Lines changed: 21 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
import json
2-
import time
2+
import textwrap
33
import unittest
44

5+
from django.core.exceptions import SuspiciousOperation
56
from django.core.serializers.base import DeserializationError, DeserializedObject
67
from django.core.serializers.json import Deserializer as JsonDeserializer
78
from django.core.serializers.jsonl import Deserializer as JsonlDeserializer
89
from django.core.serializers.python import Deserializer
910
from django.core.serializers.xml_serializer import Deserializer as XMLDeserializer
10-
from django.db import models
1111
from django.test import SimpleTestCase
12-
from django.test.utils import garbage_collect
1312

1413
from .models import Author
1514

@@ -138,52 +137,23 @@ def test_yaml_bytes_input(self):
138137
self.assertEqual(first_item.object, self.jane)
139138
self.assertEqual(second_item.object, self.joe)
140139

141-
def test_crafted_xml_performance(self):
142-
"""The time to process invalid inputs is not quadratic."""
143-
144-
def build_crafted_xml(depth, leaf_text_len):
145-
nested_open = "<nested>" * depth
146-
nested_close = "</nested>" * depth
147-
leaf = "x" * leaf_text_len
148-
field_content = f"{nested_open}{leaf}{nested_close}"
149-
return f"""
150-
<django-objects version="1.0">
151-
<object model="contenttypes.contenttype" pk="1">
152-
<field name="app_label">{field_content}</field>
153-
<field name="model">m</field>
154-
</object>
155-
</django-objects>
156-
"""
157-
158-
def deserialize(crafted_xml):
159-
iterator = XMLDeserializer(crafted_xml)
160-
garbage_collect()
161-
162-
start_time = time.perf_counter()
163-
result = list(iterator)
164-
end_time = time.perf_counter()
165-
166-
self.assertEqual(len(result), 1)
167-
self.assertIsInstance(result[0].object, models.Model)
168-
return end_time - start_time
169-
170-
def assertFactor(label, params, factor=2):
171-
factors = []
172-
prev_time = None
173-
for depth, length in params:
174-
crafted_xml = build_crafted_xml(depth, length)
175-
elapsed = deserialize(crafted_xml)
176-
if prev_time is not None:
177-
factors.append(elapsed / prev_time)
178-
prev_time = elapsed
179-
180-
with self.subTest(label):
181-
# Assert based on the average factor to reduce test flakiness.
182-
self.assertLessEqual(sum(factors) / len(factors), factor)
183-
184-
assertFactor(
185-
"varying depth, varying length",
186-
[(50, 2000), (100, 4000), (200, 8000), (400, 16000), (800, 32000)],
187-
2,
140+
def test_crafted_xml_rejected(self):
141+
depth = 100
142+
leaf_text_len = 1000
143+
nested_open = "<nested>" * depth
144+
nested_close = "</nested>" * depth
145+
leaf = "x" * leaf_text_len
146+
field_content = f"{nested_open}{leaf}{nested_close}"
147+
crafted_xml = textwrap.dedent(
148+
f"""
149+
<django-objects version="1.0">
150+
<object model="contenttypes.contenttype" pk="1">
151+
<field name="app_label">{field_content}</field>
152+
<field name="model">m</field>
153+
</object>
154+
</django-objects>"""
188155
)
189-
assertFactor("constant depth, varying length", [(100, 1), (100, 1000)], 2)
156+
157+
msg = "Unexpected element: 'nested'"
158+
with self.assertRaisesMessage(SuspiciousOperation, msg):
159+
list(XMLDeserializer(crafted_xml))

0 commit comments

Comments
 (0)