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.