Skip to content

Commit ad447fa

Browse files
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.
1 parent 59c4bdd commit ad447fa

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
@@ -284,6 +284,10 @@ rules apply:
284284
and produced an invalid document,
285285
but removing an absent attribute raised :exc:`~xml.dom.NotFoundErr`.
286286

287+
.. versionchanged:: next
288+
Namespaces are now validated in the factory methods and when setting
289+
:attr:`~xml.dom.Node.prefix` of an attribute.
290+
287291
The following interfaces have no implementation in :mod:`!xml.dom.minidom`:
288292

289293
* :class:`DOMTimeStamp`

Doc/library/xml.dom.rst

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

689689
Raise :exc:`InvalidCharacterErr` if the name is not a valid XML name.
690+
Raise :exc:`NamespaceErr` if the qualified name is malformed,
691+
if it has a prefix and the namespace URI is empty,
692+
or if the prefix is ``'xml'``
693+
and the namespace URI is not the XML namespace.
690694

691695

692696
.. method:: Document.createTextNode(data)
@@ -740,6 +744,11 @@ inherits properties from :class:`Node`.
740744
:class:`Element` object to use the newly created attribute instance.
741745

742746
Raise :exc:`InvalidCharacterErr` if the name is not a valid XML name.
747+
Raise :exc:`NamespaceErr` if the qualified name is malformed,
748+
if it has a prefix and the namespace URI is empty,
749+
if the prefix is ``'xml'`` and the namespace URI is not the XML namespace,
750+
or if the name or the prefix is ``'xmlns'``
751+
and the namespace URI is not the XMLNS namespace, or vice versa.
743752

744753

745754
.. method:: Document.getElementById(id)
@@ -905,6 +914,11 @@ of that class.
905914
Note that a qname is the whole attribute name. This is different than above.
906915

907916
Raise :exc:`InvalidCharacterErr` if the name is not a valid XML name.
917+
Raise :exc:`NamespaceErr` if the qualified name is malformed,
918+
if it has a prefix and the namespace URI is empty,
919+
if the prefix is ``'xml'`` and the namespace URI is not the XML namespace,
920+
or if the name or the prefix is ``'xmlns'``
921+
and the namespace URI is not the XMLNS namespace, or vice versa.
908922

909923

910924
.. _dom-attr-objects:

Doc/whatsnew/3.16.rst

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

705+
* :mod:`xml.dom.minidom` now validates namespaces in the factory methods
706+
:meth:`~xml.dom.Document.createElementNS`,
707+
:meth:`~xml.dom.Document.createAttributeNS`
708+
and :meth:`~xml.dom.Element.setAttributeNS`.
709+
:exc:`~xml.dom.NamespaceErr` is now raised for a malformed qualified name,
710+
for a prefix with an empty namespace, and for illegal use
711+
of the ``xml`` and ``xmlns`` prefixes.
712+
(Contributed by Serhiy Storchaka in :gh:`156665`.)
713+
705714
* :meth:`~xml.dom.minidom.Node.toprettyxml` in :mod:`xml.dom.minidom`
706715
and :func:`~xml.etree.ElementTree.indent` in :mod:`xml.etree.ElementTree`
707716
no longer add whitespace inside an element
@@ -955,6 +964,12 @@ that may require changes to your code.
955964
Attributes defaulted in the DTD are no longer omitted when parsing.
956965
(Contributed by Jason Orendorff and Serhiy Storchaka in :gh:`44871`.)
957966

967+
* :mod:`xml.dom.minidom` now raises :exc:`~xml.dom.NamespaceErr`
968+
for a malformed qualified name, for a prefix with an empty namespace,
969+
and for illegal use of the ``xml`` and ``xmlns`` prefixes.
970+
Such operations formerly succeeded and produced an invalid document.
971+
(Contributed by Serhiy Storchaka in :gh:`156665`.)
972+
958973
* :meth:`~xml.dom.minidom.Node.toprettyxml` in :mod:`xml.dom.minidom`
959974
and :func:`~xml.etree.ElementTree.indent` in :mod:`xml.etree.ElementTree`
960975
no longer add whitespace inside an element

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"),
@@ -2121,6 +2123,69 @@ def test_cdata_parsing(self):
21212123
dom2 = parseString(dom1.toprettyxml())
21222124
self.checkWholeText(dom2.getElementsByTagName('node')[0].firstChild, '</data>')
21232125

2126+
def testNamespaceErr(self):
2127+
doc = parseString("<doc/>")
2128+
elem = doc.documentElement
2129+
XML_NS = xml.dom.XML_NAMESPACE
2130+
XMLNS_NS = xml.dom.XMLNS_NAMESPACE
2131+
for namespaceURI, qname in [
2132+
(None, "p:e"), # a prefix without a namespace
2133+
("", "p:e"),
2134+
("http://xml.python.org/ns", "p:p:e"), # malformed
2135+
("http://xml.python.org/ns", "p:"),
2136+
("http://xml.python.org/ns", "p:1e"),
2137+
("http://xml.python.org/ns", "xml:e"), # the xml prefix
2138+
]:
2139+
with self.subTest(namespaceURI=namespaceURI, qname=qname):
2140+
self.assertRaises(xml.dom.NamespaceErr,
2141+
doc.createElementNS, namespaceURI, qname)
2142+
self.assertRaises(xml.dom.NamespaceErr,
2143+
doc.createAttributeNS, namespaceURI, qname)
2144+
self.assertRaises(xml.dom.NamespaceErr,
2145+
elem.setAttributeNS, namespaceURI, qname, "v")
2146+
2147+
# the xmlns name and prefix are only allowed in the XMLNS namespace
2148+
for namespaceURI, qname in [
2149+
("http://xml.python.org/ns", "xmlns"),
2150+
("http://xml.python.org/ns", "xmlns:p"),
2151+
(None, "xmlns:p"),
2152+
(XMLNS_NS, "p:a"), # and it allows nothing else
2153+
(XMLNS_NS, "a"),
2154+
]:
2155+
with self.subTest(namespaceURI=namespaceURI, qname=qname):
2156+
self.assertRaises(xml.dom.NamespaceErr,
2157+
doc.createAttributeNS, namespaceURI, qname)
2158+
self.assertRaises(xml.dom.NamespaceErr,
2159+
elem.setAttributeNS, namespaceURI, qname, "v")
2160+
2161+
# valid combinations
2162+
doc.createElementNS(None, "e")
2163+
doc.createElementNS("http://xml.python.org/ns", "p:e")
2164+
doc.createElementNS(XML_NS, "xml:e")
2165+
doc.createAttributeNS(None, "a")
2166+
doc.createAttributeNS(XML_NS, "xml:lang")
2167+
doc.createAttributeNS(XMLNS_NS, "xmlns")
2168+
doc.createAttributeNS(XMLNS_NS, "xmlns:p")
2169+
elem.setAttributeNS("http://xml.python.org/ns", "p:a", "v")
2170+
doc.unlink()
2171+
2172+
def testAttrPrefix(self):
2173+
doc = parseString("<doc/>")
2174+
attr = doc.createAttributeNS("http://xml.python.org/ns", "p:a")
2175+
self.assertRaises(xml.dom.InvalidCharacterErr,
2176+
setattr, attr, "prefix", "q:r")
2177+
self.assertRaises(xml.dom.InvalidCharacterErr,
2178+
setattr, attr, "prefix", "1q")
2179+
self.assertRaises(xml.dom.NamespaceErr,
2180+
setattr, attr, "prefix", "xml")
2181+
self.assertRaises(xml.dom.NamespaceErr,
2182+
setattr, attr, "prefix", "xmlns")
2183+
attr.prefix = "q"
2184+
self.assertEqual(attr.name, "q:a")
2185+
attr.prefix = None
2186+
self.assertEqual(attr.name, "a")
2187+
doc.unlink()
2188+
21242189
def testInvalidCharacterErr(self):
21252190
doc = parseString("<doc/>")
21262191
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

@@ -305,6 +306,35 @@ def _check_name(name):
305306
"%r is not a valid XML name" % (name,))
306307

307308

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

446476
def _set_prefix(self, prefix):
447-
nsuri = self.namespaceURI
448-
if prefix == "xmlns":
449-
if nsuri and nsuri != XMLNS_NAMESPACE:
450-
raise xml.dom.NamespaceErr(
451-
"illegal use of 'xmlns' prefix for the wrong namespace")
477+
if prefix is not None:
478+
_check_prefix(prefix, self.namespaceURI, True)
452479
self._prefix = prefix
453480
if prefix is None:
454481
newName = self.localName
@@ -807,10 +834,10 @@ def setAttribute(self, attname, value):
807834
_clear_id_cache(self)
808835

809836
def setAttributeNS(self, namespaceURI, qualifiedName, value):
837+
_check_qualified_name(namespaceURI, qualifiedName, True)
810838
prefix, localname = _nssplit(qualifiedName)
811839
attr = self.getAttributeNodeNS(namespaceURI, localname)
812840
if attr is None:
813-
_check_name(qualifiedName)
814841
attr = Attr(qualifiedName, namespaceURI, localname, prefix)
815842
attr.value = value
816843
attr.ownerDocument = self.ownerDocument
@@ -1840,14 +1867,14 @@ def createAttribute(self, qName):
18401867
return a
18411868

18421869
def createElementNS(self, namespaceURI, qualifiedName):
1843-
_check_name(qualifiedName)
1870+
_check_qualified_name(namespaceURI, qualifiedName)
18441871
prefix, localName = _nssplit(qualifiedName)
18451872
e = Element(qualifiedName, namespaceURI, prefix)
18461873
e.ownerDocument = self
18471874
return e
18481875

18491876
def createAttributeNS(self, namespaceURI, qualifiedName):
1850-
_check_name(qualifiedName)
1877+
_check_qualified_name(namespaceURI, qualifiedName, True)
18511878
prefix, localName = _nssplit(qualifiedName)
18521879
a = Attr(qualifiedName, namespaceURI, localName, prefix)
18531880
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)