https://github.com/python/cpython/commit/ad447fa0a3f9dce741bb8eaa401b8d5ddd88f868
commit: ad447fa0a3f9dce741bb8eaa401b8d5ddd88f868
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-09-13T19:43:24+03:00
summary:

gh-156665: Validate namespaces in xml.dom.minidom (GH-156666)

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.

files:
A Misc/NEWS.d/next/Library/2026-08-30-21-00-00.gh-issue-156665.Jm3Kp9.rst
M Doc/library/xml.dom.minidom.rst
M Doc/library/xml.dom.rst
M Doc/whatsnew/3.16.rst
M Lib/test/test_minidom.py
M Lib/xml/dom/minidom.py

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, 
'</data>')
 
+    def testNamespaceErr(self):
+        doc = parseString("<doc/>")
+        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("<doc/>")
+        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("<doc/>")
         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.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to