Skip to content

Commit dbb19bd

Browse files
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.
1 parent ae93e42 commit dbb19bd

6 files changed

Lines changed: 146 additions & 13 deletions

File tree

Doc/library/xml.dom.minidom.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,10 @@ rules apply:
274274
and produced an invalid document,
275275
but removing an absent attribute raised :exc:`~xml.dom.NotFoundErr`.
276276

277+
.. versionchanged:: next
278+
Namespaces are now validated in the factory methods and when setting
279+
:attr:`~xml.dom.Node.prefix` of an attribute.
280+
277281
The following interfaces have no implementation in :mod:`!xml.dom.minidom`:
278282

279283
* :class:`DOMTimeStamp`

Doc/library/xml.dom.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -695,6 +695,10 @@ inherits properties from :class:`Node`.
695695
:meth:`~Node.insertBefore` or :meth:`~Node.appendChild`.
696696

697697
Raise :exc:`InvalidCharacterErr` if the name is not a valid XML name.
698+
Raise :exc:`NamespaceErr` if the qualified name is malformed,
699+
if it has a prefix and the namespace URI is empty,
700+
or if the prefix is ``'xml'``
701+
and the namespace URI is not the XML namespace.
698702

699703

700704
.. method:: Document.createTextNode(data)
@@ -748,6 +752,11 @@ inherits properties from :class:`Node`.
748752
:class:`Element` object to use the newly created attribute instance.
749753

750754
Raise :exc:`InvalidCharacterErr` if the name is not a valid XML name.
755+
Raise :exc:`NamespaceErr` if the qualified name is malformed,
756+
if it has a prefix and the namespace URI is empty,
757+
if the prefix is ``'xml'`` and the namespace URI is not the XML namespace,
758+
or if the name or the prefix is ``'xmlns'``
759+
and the namespace URI is not the XMLNS namespace, or vice versa.
751760

752761

753762
.. method:: Document.getElementById(id)
@@ -913,6 +922,11 @@ of that class.
913922
Note that a qname is the whole attribute name. This is different than above.
914923

915924
Raise :exc:`InvalidCharacterErr` if the name is not a valid XML name.
925+
Raise :exc:`NamespaceErr` if the qualified name is malformed,
926+
if it has a prefix and the namespace URI is empty,
927+
if the prefix is ``'xml'`` and the namespace URI is not the XML namespace,
928+
or if the name or the prefix is ``'xmlns'``
929+
and the namespace URI is not the XMLNS namespace, or vice versa.
916930

917931

918932
.. _dom-attr-objects:

Doc/whatsnew/3.16.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -642,6 +642,15 @@ xml
642642
and :meth:`!Document.createEntityReference`.
643643
(Contributed by Jason Orendorff and Serhiy Storchaka in :gh:`44871`.)
644644

645+
* :mod:`xml.dom.minidom` now validates namespaces in the factory methods
646+
:meth:`~xml.dom.Document.createElementNS`,
647+
:meth:`~xml.dom.Document.createAttributeNS`
648+
and :meth:`~xml.dom.Element.setAttributeNS`.
649+
:exc:`~xml.dom.NamespaceErr` is now raised for a malformed qualified name,
650+
for a prefix with an empty namespace, and for illegal use
651+
of the ``xml`` and ``xmlns`` prefixes.
652+
(Contributed by Serhiy Storchaka in :gh:`156665`.)
653+
645654
* Add :meth:`!GetSpecifiedAttributeCount` method
646655
to the :mod:`XML parser <xml.parsers.expat>` objects.
647656
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.
881890
Attributes defaulted in the DTD are no longer omitted when parsing.
882891
(Contributed by Jason Orendorff and Serhiy Storchaka in :gh:`44871`.)
883892

893+
* :mod:`xml.dom.minidom` now raises :exc:`~xml.dom.NamespaceErr`
894+
for a malformed qualified name, for a prefix with an empty namespace,
895+
and for illegal use of the ``xml`` and ``xmlns`` prefixes.
896+
Such operations formerly succeeded and produced an invalid document.
897+
(Contributed by Serhiy Storchaka in :gh:`156665`.)
898+
884899
* On Windows, seeking a pipe now fails instead of silently appearing to
885900
succeed: :func:`os.lseek` and :meth:`~io.IOBase.seek` raise :exc:`OSError`,
886901
and :meth:`~io.IOBase.seekable` returns ``False``. As a consequence,

Lib/test/test_minidom.py

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,7 @@ def testRemoveAttrNS(self):
365365
dom = Document()
366366
child = dom.appendChild(
367367
dom.createElementNS("http://www.python.org", "python:abc"))
368-
child.setAttributeNS("http://www.w3.org", "xmlns:python",
368+
child.setAttributeNS(xml.dom.XMLNS_NAMESPACE, "xmlns:python",
369369
"http://www.python.org")
370370
child.setAttributeNS("http://www.python.org", "python:abcattr", "foo")
371371
# removing an absent attribute has no effect
@@ -472,11 +472,13 @@ def testGetAttributeNS(self):
472472
dom = Document()
473473
child = dom.appendChild(
474474
dom.createElementNS("http://www.python.org", "python:abc"))
475-
child.setAttributeNS("http://www.w3.org", "xmlns:python",
475+
child.setAttributeNS(xml.dom.XMLNS_NAMESPACE, "xmlns:python",
476476
"http://www.python.org")
477-
self.assertEqual(child.getAttributeNS("http://www.w3.org", "python"),
477+
self.assertEqual(
478+
child.getAttributeNS(xml.dom.XMLNS_NAMESPACE, "python"),
478479
'http://www.python.org')
479-
self.assertEqual(child.getAttributeNS("http://www.w3.org", "other"),
480+
self.assertEqual(
481+
child.getAttributeNS(xml.dom.XMLNS_NAMESPACE, "other"),
480482
'')
481483
child2 = child.appendChild(dom.createElement('abc'))
482484
self.assertEqual(child2.getAttributeNS("http://www.python.org", "missing"),
@@ -1786,6 +1788,69 @@ def test_cdata_parsing(self):
17861788
dom2 = parseString(dom1.toprettyxml())
17871789
self.checkWholeText(dom2.getElementsByTagName('node')[0].firstChild, '</data>')
17881790

1791+
def testNamespaceErr(self):
1792+
doc = parseString("<doc/>")
1793+
elem = doc.documentElement
1794+
XML_NS = xml.dom.XML_NAMESPACE
1795+
XMLNS_NS = xml.dom.XMLNS_NAMESPACE
1796+
for namespaceURI, qname in [
1797+
(None, "p:e"), # a prefix without a namespace
1798+
("", "p:e"),
1799+
("http://xml.python.org/ns", "p:p:e"), # malformed
1800+
("http://xml.python.org/ns", "p:"),
1801+
("http://xml.python.org/ns", "p:1e"),
1802+
("http://xml.python.org/ns", "xml:e"), # the xml prefix
1803+
]:
1804+
with self.subTest(namespaceURI=namespaceURI, qname=qname):
1805+
self.assertRaises(xml.dom.NamespaceErr,
1806+
doc.createElementNS, namespaceURI, qname)
1807+
self.assertRaises(xml.dom.NamespaceErr,
1808+
doc.createAttributeNS, namespaceURI, qname)
1809+
self.assertRaises(xml.dom.NamespaceErr,
1810+
elem.setAttributeNS, namespaceURI, qname, "v")
1811+
1812+
# the xmlns name and prefix are only allowed in the XMLNS namespace
1813+
for namespaceURI, qname in [
1814+
("http://xml.python.org/ns", "xmlns"),
1815+
("http://xml.python.org/ns", "xmlns:p"),
1816+
(None, "xmlns:p"),
1817+
(XMLNS_NS, "p:a"), # and it allows nothing else
1818+
(XMLNS_NS, "a"),
1819+
]:
1820+
with self.subTest(namespaceURI=namespaceURI, qname=qname):
1821+
self.assertRaises(xml.dom.NamespaceErr,
1822+
doc.createAttributeNS, namespaceURI, qname)
1823+
self.assertRaises(xml.dom.NamespaceErr,
1824+
elem.setAttributeNS, namespaceURI, qname, "v")
1825+
1826+
# valid combinations
1827+
doc.createElementNS(None, "e")
1828+
doc.createElementNS("http://xml.python.org/ns", "p:e")
1829+
doc.createElementNS(XML_NS, "xml:e")
1830+
doc.createAttributeNS(None, "a")
1831+
doc.createAttributeNS(XML_NS, "xml:lang")
1832+
doc.createAttributeNS(XMLNS_NS, "xmlns")
1833+
doc.createAttributeNS(XMLNS_NS, "xmlns:p")
1834+
elem.setAttributeNS("http://xml.python.org/ns", "p:a", "v")
1835+
doc.unlink()
1836+
1837+
def testAttrPrefix(self):
1838+
doc = parseString("<doc/>")
1839+
attr = doc.createAttributeNS("http://xml.python.org/ns", "p:a")
1840+
self.assertRaises(xml.dom.InvalidCharacterErr,
1841+
setattr, attr, "prefix", "q:r")
1842+
self.assertRaises(xml.dom.InvalidCharacterErr,
1843+
setattr, attr, "prefix", "1q")
1844+
self.assertRaises(xml.dom.NamespaceErr,
1845+
setattr, attr, "prefix", "xml")
1846+
self.assertRaises(xml.dom.NamespaceErr,
1847+
setattr, attr, "prefix", "xmlns")
1848+
attr.prefix = "q"
1849+
self.assertEqual(attr.name, "q:a")
1850+
attr.prefix = None
1851+
self.assertEqual(attr.name, "a")
1852+
doc.unlink()
1853+
17891854
def testInvalidCharacterErr(self):
17901855
doc = parseString("<doc/>")
17911856
impl = getDOMImplementation()

Lib/xml/dom/minidom.py

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
import xml
2020
import xml.dom
2121

22-
from xml.dom import EMPTY_NAMESPACE, EMPTY_PREFIX, XMLNS_NAMESPACE, domreg
22+
from xml.dom import (EMPTY_NAMESPACE, EMPTY_PREFIX, XML_NAMESPACE,
23+
XMLNS_NAMESPACE, domreg)
2324
from xml.dom.minicompat import *
2425
from xml.dom.xmlbuilder import DOMImplementationLS, DocumentLS
2526

@@ -302,6 +303,35 @@ def _check_name(name):
302303
"%r is not a valid XML name" % (name,))
303304

304305

306+
def _check_prefix(prefix, namespaceURI, attribute=False):
307+
if not xml.is_valid_name(prefix) or ':' in prefix:
308+
raise xml.dom.InvalidCharacterErr(
309+
"%r is not a valid namespace prefix" % (prefix,))
310+
if not namespaceURI:
311+
raise xml.dom.NamespaceErr(
312+
"cannot use the prefix %r with an empty namespace" % (prefix,))
313+
if prefix == "xml" and namespaceURI != XML_NAMESPACE:
314+
raise xml.dom.NamespaceErr(
315+
"illegal use of the 'xml' prefix for the wrong namespace")
316+
if attribute and (prefix == "xmlns") != (namespaceURI == XMLNS_NAMESPACE):
317+
raise xml.dom.NamespaceErr(
318+
"illegal use of the 'xmlns' prefix for the wrong namespace")
319+
320+
321+
def _check_qualified_name(namespaceURI, qualifiedName, attribute=False):
322+
"""Check a namespace URI and a qualified name (see DOM Level 2 Core)."""
323+
_check_name(qualifiedName)
324+
prefix, sep, localName = qualifiedName.partition(':')
325+
if sep:
326+
if not localName or ':' in localName or not xml.is_valid_name(localName):
327+
raise xml.dom.NamespaceErr(
328+
"%r is not a valid qualified name" % (qualifiedName,))
329+
_check_prefix(prefix, namespaceURI, attribute)
330+
elif attribute and (qualifiedName == "xmlns") != (namespaceURI == XMLNS_NAMESPACE):
331+
raise xml.dom.NamespaceErr(
332+
"illegal use of the 'xmlns' attribute for the wrong namespace")
333+
334+
305335
def _is_ancestor(node, other):
306336
"Returns true iff node is an ancestor of other."
307337
other = other.parentNode
@@ -441,11 +471,8 @@ def _get_prefix(self):
441471
return self._prefix
442472

443473
def _set_prefix(self, prefix):
444-
nsuri = self.namespaceURI
445-
if prefix == "xmlns":
446-
if nsuri and nsuri != XMLNS_NAMESPACE:
447-
raise xml.dom.NamespaceErr(
448-
"illegal use of 'xmlns' prefix for the wrong namespace")
474+
if prefix is not None:
475+
_check_prefix(prefix, self.namespaceURI, True)
449476
self._prefix = prefix
450477
if prefix is None:
451478
newName = self.localName
@@ -804,10 +831,10 @@ def setAttribute(self, attname, value):
804831
_clear_id_cache(self)
805832

806833
def setAttributeNS(self, namespaceURI, qualifiedName, value):
834+
_check_qualified_name(namespaceURI, qualifiedName, True)
807835
prefix, localname = _nssplit(qualifiedName)
808836
attr = self.getAttributeNodeNS(namespaceURI, localname)
809837
if attr is None:
810-
_check_name(qualifiedName)
811838
attr = Attr(qualifiedName, namespaceURI, localname, prefix)
812839
attr.value = value
813840
attr.ownerDocument = self.ownerDocument
@@ -1814,14 +1841,14 @@ def createAttribute(self, qName):
18141841
return a
18151842

18161843
def createElementNS(self, namespaceURI, qualifiedName):
1817-
_check_name(qualifiedName)
1844+
_check_qualified_name(namespaceURI, qualifiedName)
18181845
prefix, localName = _nssplit(qualifiedName)
18191846
e = Element(qualifiedName, namespaceURI, prefix)
18201847
e.ownerDocument = self
18211848
return e
18221849

18231850
def createAttributeNS(self, namespaceURI, qualifiedName):
1824-
_check_name(qualifiedName)
1851+
_check_qualified_name(namespaceURI, qualifiedName, True)
18251852
prefix, localName = _nssplit(qualifiedName)
18261853
a = Attr(qualifiedName, namespaceURI, localName, prefix)
18271854
a.ownerDocument = self
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
:mod:`xml.dom.minidom` now validates namespaces in
2+
:meth:`~xml.dom.Document.createElementNS`,
3+
:meth:`~xml.dom.Document.createAttributeNS` and
4+
:meth:`~xml.dom.Element.setAttributeNS`, and when setting
5+
:attr:`~xml.dom.Node.prefix` of an attribute.
6+
:exc:`~xml.dom.NamespaceErr` is now raised for a malformed qualified name, for
7+
a prefix with an empty namespace, and for illegal use of the ``xml`` and
8+
``xmlns`` prefixes.

0 commit comments

Comments
 (0)