Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
gh-44871: Enforce the DOM constraints on children of a document
A document can have only one element child and only one document type
child.  This was only checked in Document.appendChild(), and only for
elements, so insertBefore() and replaceChild() could add a second one.

A document fragment no longer accepts notation children, which the DOM
does not allow.
  • Loading branch information
serhiy-storchaka committed Aug 28, 2026
commit 0f79c941ad8c2f13b33470aa76b803aece98d083
4 changes: 4 additions & 0 deletions Doc/whatsnew/3.16.rst
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,8 @@ xml
It checks names passed to the factory methods,
rejects inserting a node created by another document
or making a node a descendant of itself,
rejects a second element or document type child of a document
and a notation child of a document fragment,
reports attributes defaulted in the DTD
and whether an attribute was given in the start tag,
and implements :class:`!EntityReference` nodes
Expand Down Expand Up @@ -850,6 +852,8 @@ that may require changes to your code.
for inserting a node created by another document,
and :exc:`~xml.dom.HierarchyRequestErr` for inserting a node into itself
or its descendant.
It also rejects a second element or document type child of a document
and a notation child of a document fragment.
Such operations formerly succeeded
and produced an invalid document or an endless loop.
Attributes defaulted in the DTD are no longer omitted when parsing.
Expand Down
42 changes: 42 additions & 0 deletions Lib/test/test_minidom.py
Original file line number Diff line number Diff line change
Expand Up @@ -1855,6 +1855,48 @@ def testAncestorLoops(self):
self.assertIs(grandchild.parentNode, child)
doc.unlink()

def testOnlyOneElementAndDocumentType(self):
impl = getDOMImplementation()
doc = impl.createDocument(None, "root", None)
elem = doc.documentElement
comment = doc.appendChild(doc.createComment("c"))
self.assertRaises(xml.dom.HierarchyRequestErr,
doc.appendChild, doc.createElement("x"))
self.assertRaises(xml.dom.HierarchyRequestErr,
doc.insertBefore, doc.createElement("x"), comment)
self.assertRaises(xml.dom.HierarchyRequestErr,
doc.replaceChild, doc.createElement("x"), comment)
# replacing the document element and reordering it are allowed
other = doc.createElement("other")
doc.replaceChild(other, elem)
self.assertIs(doc.documentElement, other)
doc.appendChild(other)
self.assertEqual([n.nodeName for n in doc.childNodes],
["#comment", "other"])

doc2 = impl.createDocument(None, None, None)
doctype = impl.createDocumentType("a", None, None)
doc2.appendChild(doctype)
self.assertRaises(xml.dom.HierarchyRequestErr, doc2.appendChild,
impl.createDocumentType("b", None, None))
# re-adding the same node and replacing it are allowed
doc2.appendChild(doctype)
doc2.replaceChild(impl.createDocumentType("b", None, None), doctype)
doc.unlink()
doc2.unlink()

def testDocumentFragmentChildren(self):
doc = parseString('<!DOCTYPE doc [<!NOTATION n SYSTEM "n">]><doc/>')
frag = doc.createDocumentFragment()
notation = doc.doctype.notations.item(0)
self.assertRaises(xml.dom.HierarchyRequestErr,
frag.appendChild, notation)
frag.appendChild(doc.createElement("e"))
frag.appendChild(doc.createTextNode("t"))
frag.appendChild(doc.createComment("c"))
self.assertEqual(len(frag.childNodes), 3)
doc.unlink()

def testAttrSpecified(self):
doc = parseString("<!DOCTYPE doc ["
" <!ELEMENT doc EMPTY>"
Expand Down
29 changes: 18 additions & 11 deletions Lib/xml/dom/minidom.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ def _get_lastChild(self):
if self.childNodes:
return self.childNodes[-1]

def _check_new_child(self, newChild):
def _check_new_child(self, newChild, oldChild=None):
# Common checks for insertBefore(), appendChild() and replaceChild().
doc = self.ownerDocument or self
newChildDoc = newChild.ownerDocument
Expand Down Expand Up @@ -151,7 +151,7 @@ def replaceChild(self, newChild, oldChild):
return self.insertBefore(newChild, refChild)
if newChild is oldChild:
return
self._check_new_child(newChild)
self._check_new_child(newChild, oldChild)
if newChild.parentNode is not None:
newChild.parentNode.removeChild(newChild)
try:
Expand Down Expand Up @@ -374,8 +374,7 @@ class DocumentFragment(Node):
Node.CDATA_SECTION_NODE,
Node.ENTITY_REFERENCE_NODE,
Node.PROCESSING_INSTRUCTION_NODE,
Node.COMMENT_NODE,
Node.NOTATION_NODE)
Node.COMMENT_NODE)

def __init__(self):
self.childNodes = NodeList()
Expand Down Expand Up @@ -1693,18 +1692,26 @@ def _get_strictErrorChecking(self):
def _get_version(self):
return self.version

def _check_new_child(self, newChild, oldChild=None):
Node._check_new_child(self, newChild, oldChild)
# A document can have only one element and only one document type.
if newChild.nodeType == Node.ELEMENT_NODE:
what = "document elements"
elif newChild.nodeType == Node.DOCUMENT_TYPE_NODE:
what = "document types"
else:
return
for node in self.childNodes:
if (node.nodeType == newChild.nodeType
and node is not newChild and node is not oldChild):
raise xml.dom.HierarchyRequestErr("two %s disallowed" % what)

def appendChild(self, node):
self._check_new_child(node)
if node.parentNode is not None:
# This needs to be done before the next test since this
# may *be* the document element, in which case it should
# This may *be* the document element, in which case it should
# end up re-ordered to the end.
node.parentNode.removeChild(node)

if node.nodeType == Node.ELEMENT_NODE \
and self._get_documentElement():
raise xml.dom.HierarchyRequestErr(
"two document elements disallowed")
return Node.appendChild(self, node)

def removeChild(self, oldChild):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ Inserting a node created by another document
now raises :exc:`~xml.dom.WrongDocumentErr`,
and inserting a node into itself or its descendant
now raises :exc:`~xml.dom.HierarchyRequestErr`.
A document can no longer have two element or two document type children,
and a document fragment can no longer have notation children.
Attributes defaulted in the DTD are no longer omitted when parsing,
and :attr:`!Attr.specified` now reports
whether the attribute was given in the start tag.
Expand Down
Loading