Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 6 additions & 13 deletions django/contrib/admin/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -2050,11 +2050,6 @@ def changelist_view(self, request, extra_context=None):
# me back" button on the action confirmation page.
return HttpResponseRedirect(request.get_full_path())

# If we're allowing changelist editing, we need to construct a formset
# for the changelist given all the fields to be edited. Then we'll
# use the formset to validate/process POSTed data.
formset = cl.formset = None

# Handle POSTed bulk-edit data.
if request.method == "POST" and cl.list_editable and "_save" in request.POST:
if not self.has_change_permission(request):
Expand All @@ -2063,13 +2058,11 @@ def changelist_view(self, request, extra_context=None):
modified_objects = self._get_list_editable_queryset(
request, FormSet.get_default_prefix()
)
formset = cl.formset = FormSet(
request.POST, request.FILES, queryset=modified_objects
)
if formset.is_valid():
cl.formset = FormSet(request.POST, request.FILES, queryset=modified_objects)
if cl.formset.is_valid():
changecount = 0
with transaction.atomic(using=router.db_for_write(self.model)):
for form in formset.forms:
for form in cl.formset.forms:
if form.has_changed():
obj = self.save_form(request, form, change=True)
self.save_model(request, obj, form, change=True)
Expand All @@ -2095,11 +2088,11 @@ def changelist_view(self, request, extra_context=None):
# Handle GET -- construct a formset for display.
elif cl.list_editable and self.has_change_permission(request):
FormSet = self.get_changelist_formset(request)
formset = cl.formset = FormSet(queryset=cl.result_list)
cl.formset = FormSet(queryset=cl.result_list)

# Build the list of media to be used by the formset.
if formset:
media = self.media + formset.media
if cl.formset:
media = self.media + cl.formset.media
else:
media = self.media

Expand Down
2 changes: 1 addition & 1 deletion django/contrib/admin/views/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ def __init__(
self.preserved_filters = model_admin.get_preserved_filters(request)
self.sortable_by = sortable_by
self.search_help_text = search_help_text

self.formset = None
# Get search parameters from the query string.
_search_form = self.search_form_class(request.GET)
if not _search_form.is_valid():
Expand Down
20 changes: 10 additions & 10 deletions django/core/serializers/xml_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

from django.apps import apps
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.core.exceptions import ObjectDoesNotExist, SuspiciousOperation
from django.core.serializers import base
from django.db import DEFAULT_DB_ALIAS, models
from django.utils.xmlutils import SimplerXMLGenerator, UnserializableContentError
Expand Down Expand Up @@ -411,6 +411,8 @@ def m2m_convert(n):
try:
for c in node.getElementsByTagName("object"):
values.append(m2m_convert(c))
except SuspiciousOperation:
raise
except Exception as e:
if isinstance(e, ObjectDoesNotExist) and self.handle_forward_references:
return base.DEFER_FIELD
Expand Down Expand Up @@ -439,17 +441,15 @@ def _get_model_from_node(self, node, attr):
)


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


def getInnerText(node):
"""Get the inner text of a DOM node and any children one level deep."""
# inspired by
# https://mail.python.org/pipermail/xml-sig/2005-March/011022.html
return "".join(
[
element.data
for child in node.childNodes
for element in (child, *child.childNodes)
if element.nodeType in (element.TEXT_NODE, element.CDATA_SECTION_NODE)
]
[child.data for child in node.childNodes if check_element_type(child)]
)


Expand Down
6 changes: 3 additions & 3 deletions django/db/models/fields/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1032,9 +1032,9 @@ def _get_default(self):
if self.has_db_default():
from django.db.models.expressions import DatabaseDefault

return lambda: DatabaseDefault(
self._db_default_expression, output_field=self
)
default = DatabaseDefault(self._db_default_expression, output_field=self)

return lambda: default

if (
not self.empty_strings_allowed
Expand Down
4 changes: 4 additions & 0 deletions docs/releases/6.1.txt
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,10 @@ Serialization
``()``. This ensures primary keys are serialized when using
:option:`dumpdata --natural-primary`.

* The XML deserializer now raises
:exc:`~django.core.exceptions.SuspiciousOperation` when it encounters
unexpected nested tags.

Signals
~~~~~~~

Expand Down
8 changes: 7 additions & 1 deletion docs/topics/serialization.txt
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,8 @@ lowercase name of the model ("session") separated by a dot.

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

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

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

.. versionchanged:: 6.1

:exc:`~django.core.exceptions.SuspiciousOperation` is raised when
unexpected nested tags are found.

.. _serialization-formats-json:

JSON
Expand Down
5 changes: 0 additions & 5 deletions tests/admin_changelist/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,6 @@ def test_result_list_empty_changelist_value(self):
request.user = self.superuser
m = ChildAdmin(Child, custom_site)
cl = m.get_changelist_instance(request)
cl.formset = None
template = Template(
"{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}"
)
Expand Down Expand Up @@ -285,7 +284,6 @@ def test_result_list_set_empty_value_display_on_admin_site(self):
admin.site.empty_value_display = "???"
m = ChildAdmin(Child, admin.site)
cl = m.get_changelist_instance(request)
cl.formset = None
template = Template(
"{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}"
)
Expand All @@ -310,7 +308,6 @@ def test_result_list_set_empty_value_display_in_model_admin(self):
request.user = self.superuser
m = EmptyValueChildAdmin(Child, admin.site)
cl = m.get_changelist_instance(request)
cl.formset = None
template = Template(
"{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}"
)
Expand Down Expand Up @@ -341,7 +338,6 @@ def test_result_list_html(self):
request.user = self.superuser
m = ChildAdmin(Child, custom_site)
cl = m.get_changelist_instance(request)
cl.formset = None
template = Template(
"{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}"
)
Expand Down Expand Up @@ -370,7 +366,6 @@ def test_action_checkbox_for_model_with_dunder_html(self):
request = self._mocked_authenticated_request("/grandchild/", self.superuser)
m = GrandChildAdmin(GrandChild, custom_site)
cl = m.get_changelist_instance(request)
cl.formset = None
template = Template(
"{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}"
)
Expand Down
6 changes: 3 additions & 3 deletions tests/fixtures/fixtures/fixture9.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<django-objects version="1.0">
<object pk="2" model="fixtures.visa">
<field type="CharField" name="person">
<field name="person" rel="ManyToOneRel" to="fixtures.person">
<natural>Stephane Grappelli</natural>
</field>
<field to="auth.permission" name="permissions" rel="ManyToManyRel">
Expand All @@ -19,11 +19,11 @@
</object>
<object pk="3" model="fixtures.person">
<field type="CharField" name="name">
<natural>Artist formerly known as &quot;Prince&quot;</natural>
Artist formerly known as &quot;Prince&quot;
</field>
</object>
<object pk="3" model="fixtures.visa">
<field type="CharField" name="person">
<field name="person" rel="ManyToOneRel" to="fixtures.person">
<natural>Artist formerly known as &quot;Prince&quot;</natural>
</field>
<field to="auth.permission" name="permissions" rel="ManyToManyRel">
Expand Down
3 changes: 2 additions & 1 deletion tests/fixtures/fixtures/invalid_deeply_nested_elements.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@

<object pk="1" model="fixtures.person">
<field type="CharField" name="name">
<natural>Django <em>pony</em></natural>
<!-- This <em> is unexpected & invalid -->
Django <em>pony</em>
</field>
</object>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<django-objects version="1.0">

<object pk="1" model="fixtures.book">
<field type="CharField" name="name">Music for all ages</field>
<field to="fixtures.person" name="authors" rel="ManyToManyRel">
<object>
<!-- This <em> is unexpected & invalid -->
<natural>Artist formerly known as <em>Prince</em></natural>
</object>
</field>
</object>

</django-objects>
15 changes: 8 additions & 7 deletions tests/fixtures/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from django.apps import apps
from django.contrib.sites.models import Site
from django.core import management
from django.core.exceptions import SuspiciousOperation
from django.core.files.temp import NamedTemporaryFile
from django.core.management import CommandError
from django.core.management.commands.dumpdata import ProxyModelWarning
Expand All @@ -23,7 +24,6 @@
CircularA,
CircularB,
NaturalKeyThing,
Person,
PrimaryKeyUUIDModel,
ProxySpy,
Spy,
Expand Down Expand Up @@ -522,12 +522,13 @@ def test_loading_and_dumping(self):
)

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

def test_dumpdata_with_excludes(self):
# Load fixture1 which has a site, two articles, and a category
Expand Down
72 changes: 21 additions & 51 deletions tests/serializers/test_deserialization.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import json
import time
import textwrap
import unittest

from django.core.exceptions import SuspiciousOperation
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

Expand Down Expand Up @@ -138,52 +137,23 @@ 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,
def test_crafted_xml_rejected(self):
depth = 100
leaf_text_len = 1000
nested_open = "<nested>" * depth
nested_close = "</nested>" * depth
leaf = "x" * leaf_text_len
field_content = f"{nested_open}{leaf}{nested_close}"
crafted_xml = textwrap.dedent(
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>"""
)
assertFactor("constant depth, varying length", [(100, 1), (100, 1000)], 2)

msg = "Unexpected element: 'nested'"
with self.assertRaisesMessage(SuspiciousOperation, msg):
list(XMLDeserializer(crafted_xml))