From dbb19bd630689b35e6188ee707a6594764a28c4c Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sun, 30 Aug 2026 18:20:22 +0300 Subject: [PATCH] gh-156665: Validate namespaces in xml.dom.minidom createElementNS(), createAttributeNS() and setAttributeNS() now raise NamespaceErr for a malformed qualified name, for a prefix with an empty namespace, and for illegal use of the "xml" and "xmlns" prefixes, as required by DOM Level 2 Core. Setting the prefix of an attribute is validated too. Two tests used a wrong namespace URI for xmlns attributes. --- Doc/library/xml.dom.minidom.rst | 4 + Doc/library/xml.dom.rst | 14 ++++ Doc/whatsnew/3.16.rst | 15 ++++ Lib/test/test_minidom.py | 73 ++++++++++++++++++- Lib/xml/dom/minidom.py | 45 +++++++++--- ...-08-30-21-00-00.gh-issue-156665.Jm3Kp9.rst | 8 ++ 6 files changed, 146 insertions(+), 13 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-08-30-21-00-00.gh-issue-156665.Jm3Kp9.rst diff --git a/Doc/library/xml.dom.minidom.rst b/Doc/library/xml.dom.minidom.rst index efc81f31e36a5b..46589eea55b591 100644 --- a/Doc/library/xml.dom.minidom.rst +++ b/Doc/library/xml.dom.minidom.rst @@ -274,6 +274,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 6f5904fdf35e80..a2596dae8f0a29 100644 --- a/Doc/library/xml.dom.rst +++ b/Doc/library/xml.dom.rst @@ -695,6 +695,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) @@ -748,6 +752,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) @@ -913,6 +922,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 f3ddae7a2b2fdd..4bab4b691f89c6 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -642,6 +642,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`.) + * Add :meth:`!GetSpecifiedAttributeCount` method to the :mod:`XML parser ` objects. It tells how many of the reported attributes were given in the start tag @@ -881,6 +890,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`.) + * On Windows, seeking a pipe now fails instead of silently appearing to succeed: :func:`os.lseek` and :meth:`~io.IOBase.seek` raise :exc:`OSError`, and :meth:`~io.IOBase.seekable` returns ``False``. As a consequence, diff --git a/Lib/test/test_minidom.py b/Lib/test/test_minidom.py index 3735a6046891ea..ee3a7c46216865 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"), @@ -1786,6 +1788,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 5fd3911bd3c9eb..a60ef8a44b1033 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 @@ -302,6 +303,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 @@ -441,11 +471,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 @@ -804,10 +831,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 @@ -1814,14 +1841,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.