diff --git a/Doc/library/xml.dom.minidom.rst b/Doc/library/xml.dom.minidom.rst index eb4984e2b53d92..3e1f7a7e12a94e 100644 --- a/Doc/library/xml.dom.minidom.rst +++ b/Doc/library/xml.dom.minidom.rst @@ -284,6 +284,10 @@ rules apply: and produced an invalid document, but removing an absent attribute raised :exc:`~xml.dom.NotFoundErr`. + .. versionchanged:: next + Namespaces are now validated in the factory methods and when setting + :attr:`~xml.dom.Node.prefix` of an attribute. + The following interfaces have no implementation in :mod:`!xml.dom.minidom`: * :class:`DOMTimeStamp` diff --git a/Doc/library/xml.dom.rst b/Doc/library/xml.dom.rst index b916e5ad9bd782..b1c9e27a55274b 100644 --- a/Doc/library/xml.dom.rst +++ b/Doc/library/xml.dom.rst @@ -687,6 +687,10 @@ inherits properties from :class:`Node`. :meth:`~Node.insertBefore` or :meth:`~Node.appendChild`. Raise :exc:`InvalidCharacterErr` if the name is not a valid XML name. + Raise :exc:`NamespaceErr` if the qualified name is malformed, + if it has a prefix and the namespace URI is empty, + or if the prefix is ``'xml'`` + and the namespace URI is not the XML namespace. .. method:: Document.createTextNode(data) @@ -740,6 +744,11 @@ inherits properties from :class:`Node`. :class:`Element` object to use the newly created attribute instance. Raise :exc:`InvalidCharacterErr` if the name is not a valid XML name. + Raise :exc:`NamespaceErr` if the qualified name is malformed, + if it has a prefix and the namespace URI is empty, + if the prefix is ``'xml'`` and the namespace URI is not the XML namespace, + or if the name or the prefix is ``'xmlns'`` + and the namespace URI is not the XMLNS namespace, or vice versa. .. method:: Document.getElementById(id) @@ -905,6 +914,11 @@ of that class. Note that a qname is the whole attribute name. This is different than above. Raise :exc:`InvalidCharacterErr` if the name is not a valid XML name. + Raise :exc:`NamespaceErr` if the qualified name is malformed, + if it has a prefix and the namespace URI is empty, + if the prefix is ``'xml'`` and the namespace URI is not the XML namespace, + or if the name or the prefix is ``'xmlns'`` + and the namespace URI is not the XMLNS namespace, or vice versa. .. _dom-attr-objects: diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 71ad3558b3be68..aa875362c9b628 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -702,6 +702,15 @@ xml and :meth:`!Document.createEntityReference`. (Contributed by Jason Orendorff and Serhiy Storchaka in :gh:`44871`.) +* :mod:`xml.dom.minidom` now validates namespaces in the factory methods + :meth:`~xml.dom.Document.createElementNS`, + :meth:`~xml.dom.Document.createAttributeNS` + and :meth:`~xml.dom.Element.setAttributeNS`. + :exc:`~xml.dom.NamespaceErr` is now raised for a malformed qualified name, + for a prefix with an empty namespace, and for illegal use + of the ``xml`` and ``xmlns`` prefixes. + (Contributed by Serhiy Storchaka in :gh:`156665`.) + * :meth:`~xml.dom.minidom.Node.toprettyxml` in :mod:`xml.dom.minidom` and :func:`~xml.etree.ElementTree.indent` in :mod:`xml.etree.ElementTree` no longer add whitespace inside an element @@ -955,6 +964,12 @@ that may require changes to your code. Attributes defaulted in the DTD are no longer omitted when parsing. (Contributed by Jason Orendorff and Serhiy Storchaka in :gh:`44871`.) +* :mod:`xml.dom.minidom` now raises :exc:`~xml.dom.NamespaceErr` + for a malformed qualified name, for a prefix with an empty namespace, + and for illegal use of the ``xml`` and ``xmlns`` prefixes. + Such operations formerly succeeded and produced an invalid document. + (Contributed by Serhiy Storchaka in :gh:`156665`.) + * :meth:`~xml.dom.minidom.Node.toprettyxml` in :mod:`xml.dom.minidom` and :func:`~xml.etree.ElementTree.indent` in :mod:`xml.etree.ElementTree` no longer add whitespace inside an element diff --git a/Lib/test/test_minidom.py b/Lib/test/test_minidom.py index 837a54a64379f7..3cc99e36e8898f 100644 --- a/Lib/test/test_minidom.py +++ b/Lib/test/test_minidom.py @@ -365,7 +365,7 @@ def testRemoveAttrNS(self): dom = Document() child = dom.appendChild( dom.createElementNS("http://www.python.org", "python:abc")) - child.setAttributeNS("http://www.w3.org", "xmlns:python", + child.setAttributeNS(xml.dom.XMLNS_NAMESPACE, "xmlns:python", "http://www.python.org") child.setAttributeNS("http://www.python.org", "python:abcattr", "foo") # removing an absent attribute has no effect @@ -472,11 +472,13 @@ def testGetAttributeNS(self): dom = Document() child = dom.appendChild( dom.createElementNS("http://www.python.org", "python:abc")) - child.setAttributeNS("http://www.w3.org", "xmlns:python", + child.setAttributeNS(xml.dom.XMLNS_NAMESPACE, "xmlns:python", "http://www.python.org") - self.assertEqual(child.getAttributeNS("http://www.w3.org", "python"), + self.assertEqual( + child.getAttributeNS(xml.dom.XMLNS_NAMESPACE, "python"), 'http://www.python.org') - self.assertEqual(child.getAttributeNS("http://www.w3.org", "other"), + self.assertEqual( + child.getAttributeNS(xml.dom.XMLNS_NAMESPACE, "other"), '') child2 = child.appendChild(dom.createElement('abc')) self.assertEqual(child2.getAttributeNS("http://www.python.org", "missing"), @@ -2121,6 +2123,69 @@ def test_cdata_parsing(self): dom2 = parseString(dom1.toprettyxml()) self.checkWholeText(dom2.getElementsByTagName('node')[0].firstChild, '') + def testNamespaceErr(self): + doc = parseString("") + elem = doc.documentElement + XML_NS = xml.dom.XML_NAMESPACE + XMLNS_NS = xml.dom.XMLNS_NAMESPACE + for namespaceURI, qname in [ + (None, "p:e"), # a prefix without a namespace + ("", "p:e"), + ("http://xml.python.org/ns", "p:p:e"), # malformed + ("http://xml.python.org/ns", "p:"), + ("http://xml.python.org/ns", "p:1e"), + ("http://xml.python.org/ns", "xml:e"), # the xml prefix + ]: + with self.subTest(namespaceURI=namespaceURI, qname=qname): + self.assertRaises(xml.dom.NamespaceErr, + doc.createElementNS, namespaceURI, qname) + self.assertRaises(xml.dom.NamespaceErr, + doc.createAttributeNS, namespaceURI, qname) + self.assertRaises(xml.dom.NamespaceErr, + elem.setAttributeNS, namespaceURI, qname, "v") + + # the xmlns name and prefix are only allowed in the XMLNS namespace + for namespaceURI, qname in [ + ("http://xml.python.org/ns", "xmlns"), + ("http://xml.python.org/ns", "xmlns:p"), + (None, "xmlns:p"), + (XMLNS_NS, "p:a"), # and it allows nothing else + (XMLNS_NS, "a"), + ]: + with self.subTest(namespaceURI=namespaceURI, qname=qname): + self.assertRaises(xml.dom.NamespaceErr, + doc.createAttributeNS, namespaceURI, qname) + self.assertRaises(xml.dom.NamespaceErr, + elem.setAttributeNS, namespaceURI, qname, "v") + + # valid combinations + doc.createElementNS(None, "e") + doc.createElementNS("http://xml.python.org/ns", "p:e") + doc.createElementNS(XML_NS, "xml:e") + doc.createAttributeNS(None, "a") + doc.createAttributeNS(XML_NS, "xml:lang") + doc.createAttributeNS(XMLNS_NS, "xmlns") + doc.createAttributeNS(XMLNS_NS, "xmlns:p") + elem.setAttributeNS("http://xml.python.org/ns", "p:a", "v") + doc.unlink() + + def testAttrPrefix(self): + doc = parseString("") + attr = doc.createAttributeNS("http://xml.python.org/ns", "p:a") + self.assertRaises(xml.dom.InvalidCharacterErr, + setattr, attr, "prefix", "q:r") + self.assertRaises(xml.dom.InvalidCharacterErr, + setattr, attr, "prefix", "1q") + self.assertRaises(xml.dom.NamespaceErr, + setattr, attr, "prefix", "xml") + self.assertRaises(xml.dom.NamespaceErr, + setattr, attr, "prefix", "xmlns") + attr.prefix = "q" + self.assertEqual(attr.name, "q:a") + attr.prefix = None + self.assertEqual(attr.name, "a") + doc.unlink() + def testInvalidCharacterErr(self): doc = parseString("") impl = getDOMImplementation() diff --git a/Lib/xml/dom/minidom.py b/Lib/xml/dom/minidom.py index 93c2e0638493e3..2edd3f438e686d 100644 --- a/Lib/xml/dom/minidom.py +++ b/Lib/xml/dom/minidom.py @@ -19,7 +19,8 @@ import xml import xml.dom -from xml.dom import EMPTY_NAMESPACE, EMPTY_PREFIX, XMLNS_NAMESPACE, domreg +from xml.dom import (EMPTY_NAMESPACE, EMPTY_PREFIX, XML_NAMESPACE, + XMLNS_NAMESPACE, domreg) from xml.dom.minicompat import * from xml.dom.xmlbuilder import DOMImplementationLS, DocumentLS @@ -305,6 +306,35 @@ def _check_name(name): "%r is not a valid XML name" % (name,)) +def _check_prefix(prefix, namespaceURI, attribute=False): + if not xml.is_valid_name(prefix) or ':' in prefix: + raise xml.dom.InvalidCharacterErr( + "%r is not a valid namespace prefix" % (prefix,)) + if not namespaceURI: + raise xml.dom.NamespaceErr( + "cannot use the prefix %r with an empty namespace" % (prefix,)) + if prefix == "xml" and namespaceURI != XML_NAMESPACE: + raise xml.dom.NamespaceErr( + "illegal use of the 'xml' prefix for the wrong namespace") + if attribute and (prefix == "xmlns") != (namespaceURI == XMLNS_NAMESPACE): + raise xml.dom.NamespaceErr( + "illegal use of the 'xmlns' prefix for the wrong namespace") + + +def _check_qualified_name(namespaceURI, qualifiedName, attribute=False): + """Check a namespace URI and a qualified name (see DOM Level 2 Core).""" + _check_name(qualifiedName) + prefix, sep, localName = qualifiedName.partition(':') + if sep: + if not localName or ':' in localName or not xml.is_valid_name(localName): + raise xml.dom.NamespaceErr( + "%r is not a valid qualified name" % (qualifiedName,)) + _check_prefix(prefix, namespaceURI, attribute) + elif attribute and (qualifiedName == "xmlns") != (namespaceURI == XMLNS_NAMESPACE): + raise xml.dom.NamespaceErr( + "illegal use of the 'xmlns' attribute for the wrong namespace") + + def _is_ancestor(node, other): "Returns true iff node is an ancestor of other." other = other.parentNode @@ -444,11 +474,8 @@ def _get_prefix(self): return self._prefix def _set_prefix(self, prefix): - nsuri = self.namespaceURI - if prefix == "xmlns": - if nsuri and nsuri != XMLNS_NAMESPACE: - raise xml.dom.NamespaceErr( - "illegal use of 'xmlns' prefix for the wrong namespace") + if prefix is not None: + _check_prefix(prefix, self.namespaceURI, True) self._prefix = prefix if prefix is None: newName = self.localName @@ -807,10 +834,10 @@ def setAttribute(self, attname, value): _clear_id_cache(self) def setAttributeNS(self, namespaceURI, qualifiedName, value): + _check_qualified_name(namespaceURI, qualifiedName, True) prefix, localname = _nssplit(qualifiedName) attr = self.getAttributeNodeNS(namespaceURI, localname) if attr is None: - _check_name(qualifiedName) attr = Attr(qualifiedName, namespaceURI, localname, prefix) attr.value = value attr.ownerDocument = self.ownerDocument @@ -1840,14 +1867,14 @@ def createAttribute(self, qName): return a def createElementNS(self, namespaceURI, qualifiedName): - _check_name(qualifiedName) + _check_qualified_name(namespaceURI, qualifiedName) prefix, localName = _nssplit(qualifiedName) e = Element(qualifiedName, namespaceURI, prefix) e.ownerDocument = self return e def createAttributeNS(self, namespaceURI, qualifiedName): - _check_name(qualifiedName) + _check_qualified_name(namespaceURI, qualifiedName, True) prefix, localName = _nssplit(qualifiedName) a = Attr(qualifiedName, namespaceURI, localName, prefix) a.ownerDocument = self diff --git a/Misc/NEWS.d/next/Library/2026-08-30-21-00-00.gh-issue-156665.Jm3Kp9.rst b/Misc/NEWS.d/next/Library/2026-08-30-21-00-00.gh-issue-156665.Jm3Kp9.rst new file mode 100644 index 00000000000000..b1068591078a17 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-30-21-00-00.gh-issue-156665.Jm3Kp9.rst @@ -0,0 +1,8 @@ +:mod:`xml.dom.minidom` now validates namespaces in +:meth:`~xml.dom.Document.createElementNS`, +:meth:`~xml.dom.Document.createAttributeNS` and +:meth:`~xml.dom.Element.setAttributeNS`, and when setting +:attr:`~xml.dom.Node.prefix` of an attribute. +:exc:`~xml.dom.NamespaceErr` is now raised for a malformed qualified name, for +a prefix with an empty namespace, and for illegal use of the ``xml`` and +``xmlns`` prefixes.