CVE-2021-32796: XML Injection (aka Blind XPath Injection) (CWE-91)
value: xmldom is an open source pure JavaScript W3C standard-based (XML DOM Level 2 Core) DOMParser and XMLSerializer module. xmldom versions 0.6.0 and older do not correctly escape special characters when serializing elements removed from their ancestor. This may lead to unexpected syntactic changes during XML processing in some downstream applications. This issue has been resolved in version 0.7.0. As a workaround downstream applications can validate the input and reject the maliciously crafted documents.
CWE description: The product does not properly neutralize special elements that are used in XML, allowing attackers to modify the syntax, content, or commands of the XML before it is processed by an end system.
CVSS v3 base score: 5.3
Published: 2021-07-27T22:15Z
Repository: https://github.com/xmldom/xmldom
Language: JavaScript
Files changed: lib/dom.js, test/dom/serializer.test.js
Commit date: 2021-07-27 22:32:03 +0200
Commit message: Merge pull request from GHSA-5fg8-2547-mr8q

resolves https://github.com/xmldom/xmldom/security/advisories/GHSA-5fg8-2547-mr8q
Diff: diff --git a/lib/dom.js b/lib/dom.js
index de9f606..aebcafb 100644
--- a/lib/dom.js
+++ b/lib/dom.js
@@ -1127,10 +1127,19 @@ function needNamespaceDefine(node, isHTML, visibleNamespaces) {
 			return ns.namespace !== uri;
 		}
 	}
 	return true;
 }
+/**
+ * Well-formed constraint: No < in Attribute Values
+ * The replacement text of any entity referred to directly or indirectly in an attribute value must not contain a <.
+ * @see https://www.w3.org/TR/xml/#CleanAttrVals
+ * @see https://www.w3.org/TR/xml/#NT-AttValue
+ */
+function addSerializedAttribute(buf, qualifiedName, value) {
+	buf.push(' ', qualifiedName, '="', value.replace(/[<&"]/g,_xmlEncoder), '"')
+}
 
 function serializeToString(node,buf,isHTML,nodeFilter,visibleNamespaces){
 	if (!visibleNamespaces) {
 		visibleNamespaces = [];
 	}
@@ -1194,23 +1203,21 @@ function serializeToString(node,buf,isHTML,nodeFilter,visibleNamespaces){
 		for(var i=0;i<len;i++){
 			var attr = attrs.item(i);
 			if (needNamespaceDefine(attr,isHTML, visibleNamespaces)) {
 				var prefix = attr.prefix||'';
 				var uri = attr.namespaceURI;
-				var ns = prefix ? ' xmlns:' + prefix : " xmlns";
-				buf.push(ns, '="' , uri , '"');
+				addSerializedAttribute(buf, prefix ? 'xmlns:' + prefix : "xmlns", uri);
 				visibleNamespaces.push({ prefix: prefix, namespace:uri });
 			}
 			serializeToString(attr,buf,isHTML,nodeFilter,visibleNamespaces);
 		}
 
 		// add namespace for current node		
 		if (nodeName === prefixedNodeName && needNamespaceDefine(node, isHTML, visibleNamespaces)) {
 			var prefix = node.prefix||'';
 			var uri = node.namespaceURI;
-			var ns = prefix ? ' xmlns:' + prefix : " xmlns";
-			buf.push(ns, '="' , uri , '"');
+			addSerializedAttribute(buf, prefix ? 'xmlns:' + prefix : "xmlns", uri);
 			visibleNamespaces.push({ prefix: prefix, namespace:uri });
 		}
 		
 		if(child || isHTML && !/^(?:meta|link|img|br|hr|input)$/i.test(nodeName)){
 			buf.push('>');
@@ -1245,17 +1252,11 @@ function serializeToString(node,buf,isHTML,nodeFilter,visibleNamespaces){
 			serializeToString(child, buf, isHTML, nodeFilter, visibleNamespaces.slice());
 			child = child.nextSibling;
 		}
 		return;
 	case ATTRIBUTE_NODE:
-		/**
-		 * Well-formedness constraint: No < in Attribute Values
-		 * The replacement text of any entity referred to directly or indirectly in an attribute value must not contain a <.
-		 * @see https://www.w3.org/TR/xml/#CleanAttrVals
-		 * @see https://www.w3.org/TR/xml/#NT-AttValue
-		 */
-		return buf.push(' ', node.name, '="', node.value.replace(/[<&"]/g,_xmlEncoder), '"');
+		return addSerializedAttribute(buf, node.name, node.value);
 	case TEXT_NODE:
 		/**
 		 * The ampersand character (&) and the left angle bracket (<) must not appear in their literal form,
 		 * except when used as markup delimiters, or within a comment, a processing instruction, or a CDATA section.
 		 * If they are needed elsewhere, they must be escaped using either numeric character references or the strings
diff --git a/test/dom/serializer.test.js b/test/dom/serializer.test.js
index 56eea88..1e5e98f 100644
--- a/test/dom/serializer.test.js
+++ b/test/dom/serializer.test.js
@@ -118,6 +118,25 @@ describe('XML Serializer', () => {
 			expect(new XMLSerializer().serializeToString(doc)).toBe(
 				'<a:foo xmlns:a="AAA"><child xmlns="BBB"><nested/></child></a:foo>'
 			)
 		})
 	})
+	describe('properly escapes attribute values', () => {
+		it('should escape special characters in namespace attributes', () => {
+			const input = `<xml xmlns='<&"' xmlns:attr='"&<'><test attr:test=""/></xml>`
+			const doc = new DOMParser().parseFromString(input, MIME_TYPE.XML_TEXT)
+
+			// in this case the explicit attribute nodes are serialized
+			expect(new XMLSerializer().serializeToString(doc)).toBe(
+				'<xml xmlns="&lt;&amp;&quot;" xmlns:attr="&quot;&amp;&lt;"><test attr:test=""/></xml>'
+			)
+
+			// in this case the namespace attributes are "inherited" from the parent,
+			// which is not serialized
+			expect(
+				new XMLSerializer().serializeToString(doc.documentElement.firstChild)
+			).toBe(
+				'<test xmlns:attr="&quot;&amp;&lt;" attr:test="" xmlns="&lt;&amp;&quot;"/>'
+			)
+		})
+	})
 })

Vulnerable code: 				var ns = prefix ? ' xmlns:' + prefix : " xmlns";
				buf.push(ns, '="' , uri , '"');
			var ns = prefix ? ' xmlns:' + prefix : " xmlns";
			buf.push(ns, '="' , uri , '"');
		/**
		 * Well-formedness constraint: No < in Attribute Values
		 * The replacement text of any entity referred to directly or indirectly in an attribute value must not contain a <.
		 * @see https://www.w3.org/TR/xml/#CleanAttrVals
		 * @see https://www.w3.org/TR/xml/#NT-AttValue
		 */
		return buf.push(' ', node.name, '="', node.value.replace(/[<&"]/g,_xmlEncoder), '"');
Fixed code: /**
 * Well-formed constraint: No < in Attribute Values
 * The replacement text of any entity referred to directly or indirectly in an attribute value must not contain a <.
 * @see https://www.w3.org/TR/xml/#CleanAttrVals
 * @see https://www.w3.org/TR/xml/#NT-AttValue
 */
function addSerializedAttribute(buf, qualifiedName, value) {
	buf.push(' ', qualifiedName, '="', value.replace(/[<&"]/g,_xmlEncoder), '"')
}
				addSerializedAttribute(buf, prefix ? 'xmlns:' + prefix : "xmlns", uri);
			addSerializedAttribute(buf, prefix ? 'xmlns:' + prefix : "xmlns", uri);
		return addSerializedAttribute(buf, node.name, node.value);
	describe('properly escapes attribute values', () => {
		it('should escape special characters in namespace attributes', () => {
			const input = `<xml xmlns='<&"' xmlns:attr='"&<'><test attr:test=""/></xml>`
			const doc = new DOMParser().parseFromString(input, MIME_TYPE.XML_TEXT)

			// in this case the explicit attribute nodes are serialized
			expect(new XMLSerializer().serializeToString(doc)).toBe(
				'<xml xmlns="&lt;&amp;&quot;" xmlns:attr="&quot;&amp;&lt;"><test attr:test=""/></xml>'
			)

			// in this case the namespace attributes are "inherited" from the parent,
			// which is not serialized
			expect(
				new XMLSerializer().serializeToString(doc.documentElement.firstChild)
			).toBe(
				'<test xmlns:attr="&quot;&amp;&lt;" attr:test="" xmlns="&lt;&amp;&quot;"/>'
			)
		})
	})