VYPR
Critical severityNVD Advisory· Published Nov 2, 2022· Updated Apr 22, 2025

xmldom allows multiple root nodes in a DOM

CVE-2022-39353

Description

xmldom is a pure JavaScript W3C standard-based (XML DOM Level 2 Core) DOMParser and XMLSerializer module. xmldom parses XML that is not well-formed because it contains multiple top level elements, and adds all root nodes to the childNodes collection of the Document, without reporting any error or throwing. This breaks the assumption that there is only a single root node in the tree, which led to issuance of CVE-2022-39299 as it is a potential issue for dependents. Update to @xmldom/xmldom@~0.7.7, @xmldom/xmldom@~0.8.4 (dist-tag latest) or @xmldom/xmldom@>=0.9.0-beta.4 (dist-tag next). As a workaround, please one of the following approaches depending on your use case: instead of searching for elements in the whole DOM, only search in the documentElementor reject a document with a document that has more then 1 childNode.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
xmldomnpm
<= 0.6.0
@xmldom/xmldomnpm
< 0.7.70.7.7
@xmldom/xmldomnpm
>= 0.8.0, < 0.8.40.8.4
@xmldom/xmldomnpm
>= 0.9.0-beta.1, < 0.9.0-beta.40.9.0-beta.4

Affected products

1

Patches

3
7ff7c10ab296

Merge pull request from GHSA-crh6-fp67-6883

https://github.com/xmldom/xmldomChristian BewernitzOct 29, 2022via ghsa
4 files changed · +290 58
  • lib/dom.js+203 48 modified
    @@ -158,14 +158,14 @@ NodeList.prototype = {
     	 * The number of nodes in the list. The range of valid child node indices is 0 to length-1 inclusive.
     	 * @standard level1
     	 */
    -	length:0, 
    +	length:0,
     	/**
     	 * Returns the indexth item in the collection. If index is greater than or equal to the number of nodes in the list, this returns null.
     	 * @standard level1
    -	 * @param index  unsigned long 
    +	 * @param index  unsigned long
     	 *   Index into the collection.
     	 * @return Node
    -	 * 	The node at the indexth position in the NodeList, or null if that is not a valid index. 
    +	 * 	The node at the indexth position in the NodeList, or null if that is not a valid index.
     	 */
     	item: function(index) {
     		return this[index] || null;
    @@ -175,7 +175,31 @@ NodeList.prototype = {
     			serializeToString(this[i],buf,isHTML,nodeFilter);
     		}
     		return buf.join('');
    -	}
    +	},
    +	/**
    +	 * @private
    +	 * @param {function (Node):boolean} predicate
    +	 * @returns {Node | undefined}
    +	 */
    +	find: function (predicate) {
    +		return Array.prototype.find.call(this, predicate);
    +	},
    +	/**
    +	 * @private
    +	 * @param {function (Node):boolean} predicate
    +	 * @returns {Node[]}
    +	 */
    +	filter: function (predicate) {
    +		return Array.prototype.filter.call(this, predicate);
    +	},
    +	/**
    +	 * @private
    +	 * @param {Node} item
    +	 * @returns {number}
    +	 */
    +	indexOf: function (item) {
    +		return Array.prototype.indexOf.call(this, item);
    +	},
     };
     
     function LiveNodeList(node,refresh){
    @@ -209,7 +233,7 @@ _extends(LiveNodeList,NodeList);
      * but this is simply to allow convenient enumeration of the contents of a NamedNodeMap,
      * and does not imply that the DOM specifies an order to these Nodes.
      * NamedNodeMap objects in the DOM are live.
    - * used for attributes or DocumentType entities 
    + * used for attributes or DocumentType entities
      */
     function NamedNodeMap() {
     };
    @@ -253,7 +277,7 @@ function _removeNamedNode(el,list,attr){
     			}
     		}
     	}else{
    -		throw DOMException(NOT_FOUND_ERR,new Error(el.tagName+'@'+attr))
    +		throw new DOMException(NOT_FOUND_ERR,new Error(el.tagName+'@'+attr))
     	}
     }
     NamedNodeMap.prototype = {
    @@ -298,10 +322,10 @@ NamedNodeMap.prototype = {
     		var attr = this.getNamedItem(key);
     		_removeNamedNode(this._ownerElement,this,attr);
     		return attr;
    -		
    -		
    +
    +
     	},// raises: NOT_FOUND_ERR,NO_MODIFICATION_ALLOWED_ERR
    -	
    +
     	//for level2
     	removeNamedItemNS:function(namespaceURI,localName){
     		var attr = this.getNamedItemNS(namespaceURI,localName);
    @@ -447,10 +471,10 @@ Node.prototype = {
     	prefix : null,
     	localName : null,
     	// Modified in DOM Level 2:
    -	insertBefore:function(newChild, refChild){//raises 
    +	insertBefore:function(newChild, refChild){//raises
     		return _insertBefore(this,newChild,refChild);
     	},
    -	replaceChild:function(newChild, oldChild){//raises 
    +	replaceChild:function(newChild, oldChild){//raises
     		this.insertBefore(newChild,oldChild);
     		if(oldChild){
     			this.removeChild(oldChild);
    @@ -656,48 +680,177 @@ function _removeChild (parentNode, child) {
     	_onUpdateChild(parentNode.ownerDocument, parentNode);
     	return child;
     }
    +
    +/**
    + * Returns `true` if `node` can be a parent for insertion.
    + * @param {Node} node
    + * @returns {boolean}
    + */
    +function hasValidParentNodeType(node) {
    +	return (
    +		node &&
    +		(node.nodeType === Node.DOCUMENT_NODE || node.nodeType === Node.DOCUMENT_FRAGMENT_NODE || node.nodeType === Node.ELEMENT_NODE)
    +	);
    +}
    +
    +/**
    + * Returns `true` if `node` can be inserted according to it's `nodeType`.
    + * @param {Node} node
    + * @returns {boolean}
    + */
    +function hasInsertableNodeType(node) {
    +	return (
    +		node &&
    +		(isElementNode(node) ||
    +			isTextNode(node) ||
    +			isDocTypeNode(node) ||
    +			node.nodeType === Node.DOCUMENT_FRAGMENT_NODE ||
    +			node.nodeType === Node.COMMENT_NODE ||
    +			node.nodeType === Node.PROCESSING_INSTRUCTION_NODE)
    +	);
    +}
    +
    +/**
    + * Returns true if `node` is a DOCTYPE node
    + * @param {Node} node
    + * @returns {boolean}
    + */
    +function isDocTypeNode(node) {
    +	return node && node.nodeType === Node.DOCUMENT_TYPE_NODE;
    +}
    +
    +/**
    + * Returns true if the node is an element
    + * @param {Node} node
    + * @returns {boolean}
    + */
    +function isElementNode(node) {
    +	return node && node.nodeType === Node.ELEMENT_NODE;
    +}
    +/**
    + * Returns true if `node` is a text node
    + * @param {Node} node
    + * @returns {boolean}
    + */
    +function isTextNode(node) {
    +	return node && node.nodeType === Node.TEXT_NODE;
    +}
    +
    +/**
    + * Check if en element node can be inserted before `child`, or at the end if child is falsy,
    + * according to the presence and position of a doctype node on the same level.
    + *
    + * @param {Document} doc The document node
    + * @param {Node} child the node that would become the nextSibling if the element would be inserted
    + * @returns {boolean} `true` if an element can be inserted before child
    + * @private
    + * https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity
    + */
    +function isElementInsertionPossible(doc, child) {
    +	var parentChildNodes = doc.childNodes || [];
    +	if (parentChildNodes.find(isElementNode) || isDocTypeNode(child)) {
    +		return false;
    +	}
    +	var docTypeNode = parentChildNodes.find(isDocTypeNode);
    +	return !(child && docTypeNode && parentChildNodes.indexOf(docTypeNode) > parentChildNodes.indexOf(child));
    +}
     /**
    - * preformance key(refChild == null)
    + * @private
    + * @param {Node} parent the parent node to insert `node` into
    + * @param {Node} node the node to insert
    + * @param {Node=} child the node that should become the `nextSibling` of `node`
    + * @returns {Node}
    + * @throws DOMException for several node combinations that would create a DOM that is not well-formed.
    + * @throws DOMException if `child` is provided but is not a child of `parent`.
    + * @see https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity
      */
    -function _insertBefore(parentNode,newChild,nextChild){
    -	var cp = newChild.parentNode;
    +function _insertBefore(parent, node, child) {
    +	if (!hasValidParentNodeType(parent)) {
    +		throw new DOMException(HIERARCHY_REQUEST_ERR, 'Unexpected parent node type ' + parent.nodeType);
    +	}
    +	if (child && child.parentNode !== parent) {
    +		throw new DOMException(NOT_FOUND_ERR, 'child not in parent');
    +	}
    +	if (
    +		!hasInsertableNodeType(node) ||
    +		// the sax parser currently adds top level text nodes, this will be fixed in 0.9.0
    +		// || (node.nodeType === Node.TEXT_NODE && parent.nodeType === Node.DOCUMENT_NODE)
    +		(isDocTypeNode(node) && parent.nodeType !== Node.DOCUMENT_NODE)
    +	) {
    +		throw new DOMException(
    +			HIERARCHY_REQUEST_ERR,
    +			'Unexpected node type ' + node.nodeType + ' for parent node type ' + parent.nodeType
    +		);
    +	}
    +	var parentChildNodes = parent.childNodes || [];
    +	var nodeChildNodes = node.childNodes || [];
    +	if (parent.nodeType === Node.DOCUMENT_NODE) {
    +		if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
    +			let nodeChildElements = nodeChildNodes.filter(isElementNode);
    +			if (nodeChildElements.length > 1 || nodeChildNodes.find(isTextNode)) {
    +				throw new DOMException(HIERARCHY_REQUEST_ERR, 'More than one element or text in fragment');
    +			}
    +			if (nodeChildElements.length === 1 && !isElementInsertionPossible(parent, child)) {
    +				throw new DOMException(HIERARCHY_REQUEST_ERR, 'Element in fragment can not be inserted before doctype');
    +			}
    +		}
    +		if (isElementNode(node)) {
    +			if (parentChildNodes.find(isElementNode) || !isElementInsertionPossible(parent, child)) {
    +				throw new DOMException(HIERARCHY_REQUEST_ERR, 'Only one element can be added and only after doctype');
    +			}
    +		}
    +		if (isDocTypeNode(node)) {
    +			if (parentChildNodes.find(isDocTypeNode)) {
    +				throw new DOMException(HIERARCHY_REQUEST_ERR, 'Only one doctype is allowed');
    +			}
    +			let parentElementChild = parentChildNodes.find(isElementNode);
    +			if (child && parentChildNodes.indexOf(parentElementChild) < parentChildNodes.indexOf(child)) {
    +				throw new DOMException(HIERARCHY_REQUEST_ERR, 'Doctype can only be inserted before an element');
    +			}
    +			if (!child && parentElementChild) {
    +				throw new DOMException(HIERARCHY_REQUEST_ERR, 'Doctype can not be appended since element is present');
    +			}
    +		}
    +	}
    +
    +	var cp = node.parentNode;
     	if(cp){
    -		cp.removeChild(newChild);//remove and update
    +		cp.removeChild(node);//remove and update
     	}
    -	if(newChild.nodeType === DOCUMENT_FRAGMENT_NODE){
    -		var newFirst = newChild.firstChild;
    +	if(node.nodeType === DOCUMENT_FRAGMENT_NODE){
    +		var newFirst = node.firstChild;
     		if (newFirst == null) {
    -			return newChild;
    +			return node;
     		}
    -		var newLast = newChild.lastChild;
    +		var newLast = node.lastChild;
     	}else{
    -		newFirst = newLast = newChild;
    +		newFirst = newLast = node;
     	}
    -	var pre = nextChild ? nextChild.previousSibling : parentNode.lastChild;
    +	var pre = child ? child.previousSibling : parent.lastChild;
     
     	newFirst.previousSibling = pre;
    -	newLast.nextSibling = nextChild;
    -	
    -	
    +	newLast.nextSibling = child;
    +
    +
     	if(pre){
     		pre.nextSibling = newFirst;
     	}else{
    -		parentNode.firstChild = newFirst;
    +		parent.firstChild = newFirst;
     	}
    -	if(nextChild == null){
    -		parentNode.lastChild = newLast;
    +	if(child == null){
    +		parent.lastChild = newLast;
     	}else{
    -		nextChild.previousSibling = newLast;
    +		child.previousSibling = newLast;
     	}
     	do{
    -		newFirst.parentNode = parentNode;
    +		newFirst.parentNode = parent;
     	}while(newFirst !== newLast && (newFirst= newFirst.nextSibling))
    -	_onUpdateChild(parentNode.ownerDocument||parentNode,parentNode);
    -	//console.log(parentNode.lastChild.nextSibling == null)
    -	if (newChild.nodeType == DOCUMENT_FRAGMENT_NODE) {
    -		newChild.firstChild = newChild.lastChild = null;
    +	_onUpdateChild(parent.ownerDocument||parent, parent);
    +	//console.log(parent.lastChild.nextSibling == null)
    +	if (node.nodeType == DOCUMENT_FRAGMENT_NODE) {
    +		node.firstChild = node.lastChild = null;
     	}
    -	return newChild;
    +	return node;
     }
     
     /**
    @@ -752,11 +905,13 @@ Document.prototype = {
     			}
     			return newChild;
     		}
    -		if(this.documentElement == null && newChild.nodeType == ELEMENT_NODE){
    +		_insertBefore(this, newChild, refChild);
    +		newChild.ownerDocument = this;
    +		if (this.documentElement === null && newChild.nodeType === ELEMENT_NODE) {
     			this.documentElement = newChild;
     		}
     
    -		return _insertBefore(this,newChild,refChild),(newChild.ownerDocument = this),newChild;
    +		return newChild;
     	},
     	removeChild :  function(oldChild){
     		if(this.documentElement == oldChild){
    @@ -950,7 +1105,7 @@ Element.prototype = {
     		var attr = this.getAttributeNode(name)
     		attr && this.removeAttributeNode(attr);
     	},
    -	
    +
     	//four real opeartion method
     	appendChild:function(newChild){
     		if(newChild.nodeType === DOCUMENT_FRAGMENT_NODE){
    @@ -974,7 +1129,7 @@ Element.prototype = {
     		var old = this.getAttributeNodeNS(namespaceURI, localName);
     		old && this.removeAttributeNode(old);
     	},
    -	
    +
     	hasAttributeNS : function(namespaceURI, localName){
     		return this.getAttributeNodeNS(namespaceURI, localName)!=null;
     	},
    @@ -990,7 +1145,7 @@ Element.prototype = {
     	getAttributeNodeNS : function(namespaceURI, localName){
     		return this.attributes.getNamedItemNS(namespaceURI, localName);
     	},
    -	
    +
     	getElementsByTagName : function(tagName){
     		return new LiveNodeList(this,function(base){
     			var ls = [];
    @@ -1011,7 +1166,7 @@ Element.prototype = {
     				}
     			});
     			return ls;
    -			
    +
     		});
     	}
     };
    @@ -1040,7 +1195,7 @@ CharacterData.prototype = {
     	},
     	insertData: function(offset,text) {
     		this.replaceData(offset,0,text);
    -	
    +
     	},
     	appendChild:function(newChild){
     		throw new Error(ExceptionMessage[HIERARCHY_REQUEST_ERR])
    @@ -1134,7 +1289,7 @@ function nodeSerializeToString(isHtml,nodeFilter){
     	var refNode = this.nodeType == 9 && this.documentElement || this;
     	var prefix = refNode.prefix;
     	var uri = refNode.namespaceURI;
    -	
    +
     	if(uri && prefix == null){
     		//console.log(prefix)
     		var prefix = refNode.lookupPrefix(uri);
    @@ -1167,8 +1322,8 @@ function needNamespaceDefine(node, isHTML, visibleNamespaces) {
     	if (prefix === "xml" && uri === NAMESPACE.XML || uri === NAMESPACE.XMLNS) {
     		return false;
     	}
    -	
    -	var i = visibleNamespaces.length 
    +
    +	var i = visibleNamespaces.length
     	while (i--) {
     		var ns = visibleNamespaces[i];
     		// get namespace prefix
    @@ -1219,7 +1374,7 @@ function serializeToString(node,buf,isHTML,nodeFilter,visibleNamespaces){
     		var len = attrs.length;
     		var child = node.firstChild;
     		var nodeName = node.tagName;
    -		
    +
     		isHTML = NAMESPACE.isHTML(node.namespaceURI) || isHTML
     
     		var prefixedNodeName = nodeName
    @@ -1278,14 +1433,14 @@ function serializeToString(node,buf,isHTML,nodeFilter,visibleNamespaces){
     			serializeToString(attr,buf,isHTML,nodeFilter,visibleNamespaces);
     		}
     
    -		// add namespace for current node		
    +		// add namespace for current node
     		if (nodeName === prefixedNodeName && needNamespaceDefine(node, isHTML, visibleNamespaces)) {
     			var prefix = node.prefix||'';
     			var uri = node.namespaceURI;
     			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('>');
     			//if is cdata child node
    @@ -1500,7 +1655,7 @@ try{
     				}
     			}
     		})
    -		
    +
     		function getTextContent(node){
     			switch(node.nodeType){
     			case ELEMENT_NODE:
    
  • test/dom/document.test.js+67 3 modified
    @@ -1,7 +1,7 @@
    -'use strict'
    +'use strict';
     
    -const { getTestParser } = require('../get-test-parser')
    -const { DOMImplementation } = require('../../lib/dom')
    +const { getTestParser } = require('../get-test-parser');
    +const { DOMImplementation, DOMException } = require('../../lib/dom');
     
     const INPUT = (first = '', second = '', third = '', fourth = '') => `
     <html >
    @@ -99,4 +99,68 @@ describe('Document.prototype', () => {
     			expect(doc.firstChild === doctype).toBe(true)
     		})
     	})
    +	describe('insertBefore', () => {
    +		it('should insert the first element and set `documentElement`', () => {
    +			const doc = new DOMImplementation().createDocument(null, '');
    +			expect(doc.childNodes).toHaveLength(0);
    +			expect(doc.documentElement).toBeNull();
    +			const root = doc.createElement('root');
    +			doc.insertBefore(root);
    +			expect(doc.documentElement).toBe(root);
    +			expect(doc.childNodes).toHaveLength(1);
    +			expect(doc.childNodes.item(0)).toBe(root);
    +		});
    +		it('should prevent inserting a second element', () => {
    +			const doc = new DOMImplementation().createDocument(null, '');
    +			const root = doc.createElement('root');
    +			const second = doc.createElement('second');
    +			doc.insertBefore(root);
    +			expect(() => doc.insertBefore(second)).toThrow(DOMException);
    +			expect(doc.documentElement).toBe(root);
    +			expect(doc.childNodes).toHaveLength(1);
    +		});
    +		it('should prevent inserting an element before a doctype', () => {
    +			const impl = new DOMImplementation();
    +			const doctype = impl.createDocumentType('DT');
    +			const doc = impl.createDocument(null, '', doctype);
    +			expect(doc.childNodes).toHaveLength(1);
    +			const root = doc.createElement('root');
    +			expect(() => doc.insertBefore(root, doctype)).toThrow(DOMException);
    +			expect(doc.documentElement).toBeNull();
    +			expect(doc.childNodes).toHaveLength(1);
    +			expect(root.parentNode).toBeNull();
    +		});
    +		it('should prevent inserting a second doctype', () => {
    +			const impl = new DOMImplementation();
    +			const doctype = impl.createDocumentType('DT');
    +			const doctype2 = impl.createDocumentType('DT2');
    +			const doc = impl.createDocument(null, '', doctype);
    +			expect(doc.childNodes).toHaveLength(1);
    +			expect(() => doc.insertBefore(doctype2)).toThrow(DOMException);
    +			expect(doc.childNodes).toHaveLength(1);
    +		});
    +		it('should prevent inserting a doctype after an element', () => {
    +			const impl = new DOMImplementation();
    +			const doc = impl.createDocument(null, '');
    +			const root = doc.createElement('root');
    +			doc.insertBefore(root);
    +			const doctype = impl.createDocumentType('DT');
    +			expect(doc.childNodes).toHaveLength(1);
    +
    +			expect(() => doc.insertBefore(doctype)).toThrow(DOMException);
    +
    +			expect(doc.childNodes).toHaveLength(1);
    +		});
    +		it('should prevent inserting before an child which is not a child of parent', () => {
    +			const doc = new DOMImplementation().createDocument(null, '');
    +			const root = doc.createElement('root');
    +			const withoutParent = doc.createElement('second');
    +
    +			expect(() => doc.insertBefore(root, withoutParent)).toThrow(DOMException);
    +
    +			expect(doc.documentElement).toBeNull();
    +			expect(doc.childNodes).toHaveLength(0);
    +			expect(root.parentNode).toBeNull();
    +		});
    +	});
     })
    
  • test/parse/parse-element.test.js+4 4 modified
    @@ -41,10 +41,10 @@ describe('XML Node Parse', () => {
     
     	it('sibling closing tag with whitespace', () => {
     		const actual = new DOMParser()
    -			.parseFromString(`<book></book ><title>Harry Potter</title>`, 'text/xml')
    -			.toString()
    -		expect(actual).toBe(`<book/><title>Harry Potter</title>`)
    -	})
    +			.parseFromString(`<xml><book></book ><title>Harry Potter</title></xml>`, 'text/xml')
    +			.toString();
    +		expect(actual).toBe(`<xml><book/><title>Harry Potter</title></xml>`);
    +	});
     
     	describe('simple attributes', () => {
     		describe('nothing special', () => {
    
  • test/xmltest/__snapshots__/not-wf.test.js.snap+16 3 modified
    @@ -315,14 +315,22 @@ Object {
     exports[`xmltest/not-wellformed standalone should match 040.xml with snapshot 1`] = `
     Object {
       "actual": "<doc/>
    -<doc/>",
    +&lt;doc&gt;",
    +  "error": Array [
    +    "[xmldom error]	element parse error: Error: Hierarchy request error: Only one element can be added and only after doctype
    +@#[line:2,col:1]",
    +  ],
     }
     `;
     
     exports[`xmltest/not-wellformed standalone should match 041.xml with snapshot 1`] = `
     Object {
       "actual": "<doc/>
    -<doc/>",
    +&lt;doc&gt;",
    +  "error": Array [
    +    "[xmldom error]	element parse error: Error: Hierarchy request error: Only one element can be added and only after doctype
    +@#[line:2,col:1]",
    +  ],
     }
     `;
     
    @@ -342,7 +350,12 @@ Illegal data
     
     exports[`xmltest/not-wellformed standalone should match 044.xml with snapshot 1`] = `
     Object {
    -  "actual": "<doc/><doc/>",
    +  "actual": "<doc/>&lt;doc/&gt;
    +",
    +  "error": Array [
    +    "[xmldom error]	element parse error: Error: Hierarchy request error: Only one element can be added and only after doctype
    +@#[line:1,col:7]",
    +  ],
     }
     `;
     
    
52a708360c35

Merge pull request from GHSA-crh6-fp67-6883

https://github.com/xmldom/xmldomChristian BewernitzOct 29, 2022via ghsa
5 files changed · +270 35
  • lib/dom.js+180 24 modified
    @@ -180,6 +180,30 @@ NodeList.prototype = {
     		}
     		return buf.join('');
     	},
    +	/**
    +	 * @private
    +	 * @param {function (Node):boolean} predicate
    +	 * @returns {Node | undefined}
    +	 */
    +	find: function (predicate) {
    +		return Array.prototype.find.call(this, predicate);
    +	},
    +	/**
    +	 * @private
    +	 * @param {function (Node):boolean} predicate
    +	 * @returns {Node[]}
    +	 */
    +	filter: function (predicate) {
    +		return Array.prototype.filter.call(this, predicate);
    +	},
    +	/**
    +	 * @private
    +	 * @param {Node} item
    +	 * @returns {number}
    +	 */
    +	indexOf: function (item) {
    +		return Array.prototype.indexOf.call(this, item);
    +	},
     };
     
     function LiveNodeList(node, refresh) {
    @@ -455,7 +479,7 @@ DOMImplementation.prototype = {
     		doc.childNodes = new NodeList();
     		if (title !== false) {
     			doc.doctype = this.createDocumentType('html');
    -			doc.doctype.ownerDocument = this;
    +			doc.doctype.ownerDocument = doc;
     			doc.appendChild(doc.doctype);
     			var htmlNode = doc.createElement('html');
     			doc.appendChild(htmlNode);
    @@ -636,6 +660,7 @@ function _visitNode(node, callback) {
      */
     function Document(options) {
     	var opt = options || {};
    +	this.ownerDocument = this;
     	/**
     	 * The mime type of the document is determined at creation time and can not be modified.
     	 *
    @@ -739,47 +764,176 @@ function _removeChild(parentNode, child) {
     	_onUpdateChild(parentNode.ownerDocument, parentNode);
     	return child;
     }
    +
    +/**
    + * Returns `true` if `node` can be a parent for insertion.
    + * @param {Node} node
    + * @returns {boolean}
    + */
    +function hasValidParentNodeType(node) {
    +	return (
    +		node &&
    +		(node.nodeType === Node.DOCUMENT_NODE || node.nodeType === Node.DOCUMENT_FRAGMENT_NODE || node.nodeType === Node.ELEMENT_NODE)
    +	);
    +}
    +
    +/**
    + * Returns `true` if `node` can be inserted according to it's `nodeType`.
    + * @param {Node} node
    + * @returns {boolean}
    + */
    +function hasInsertableNodeType(node) {
    +	return (
    +		node &&
    +		(isElementNode(node) ||
    +			isTextNode(node) ||
    +			isDocTypeNode(node) ||
    +			node.nodeType === Node.DOCUMENT_FRAGMENT_NODE ||
    +			node.nodeType === Node.COMMENT_NODE ||
    +			node.nodeType === Node.PROCESSING_INSTRUCTION_NODE)
    +	);
    +}
    +
     /**
    - * preformance key(refChild == null)
    + * Returns true if `node` is a DOCTYPE node
    + * @param {Node} node
    + * @returns {boolean}
    + */
    +function isDocTypeNode(node) {
    +	return node && node.nodeType === Node.DOCUMENT_TYPE_NODE;
    +}
    +
    +/**
    + * Returns true if the node is an element
    + * @param {Node} node
    + * @returns {boolean}
      */
    -function _insertBefore(parentNode, newChild, nextChild) {
    -	var cp = newChild.parentNode;
    +function isElementNode(node) {
    +	return node && node.nodeType === Node.ELEMENT_NODE;
    +}
    +/**
    + * Returns true if `node` is a text node
    + * @param {Node} node
    + * @returns {boolean}
    + */
    +function isTextNode(node) {
    +	return node && node.nodeType === Node.TEXT_NODE;
    +}
    +
    +/**
    + * Check if en element node can be inserted before `child`, or at the end if child is falsy,
    + * according to the presence and position of a doctype node on the same level.
    + *
    + * @param {Document} doc The document node
    + * @param {Node} child the node that would become the nextSibling if the element would be inserted
    + * @returns {boolean} `true` if an element can be inserted before child
    + * @private
    + * https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity
    + */
    +function isElementInsertionPossible(doc, child) {
    +	var parentChildNodes = doc.childNodes || [];
    +	if (parentChildNodes.find(isElementNode) || isDocTypeNode(child)) {
    +		return false;
    +	}
    +	var docTypeNode = parentChildNodes.find(isDocTypeNode);
    +	return !(child && docTypeNode && parentChildNodes.indexOf(docTypeNode) > parentChildNodes.indexOf(child));
    +}
    +/**
    + * @private
    + * @param {Node} parent the parent node to insert `node` into
    + * @param {Node} node the node to insert
    + * @param {Node=} child the node that should become the `nextSibling` of `node`
    + * @returns {Node}
    + * @throws DOMException for several node combinations that would create a DOM that is not well-formed.
    + * @throws DOMException if `child` is provided but is not a child of `parent`.
    + * @see https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity
    + */
    +function _insertBefore(parent, node, child) {
    +	if (!hasValidParentNodeType(parent)) {
    +		throw new DOMException(HIERARCHY_REQUEST_ERR, 'Unexpected parent node type ' + parent.nodeType);
    +	}
    +	if (child && child.parentNode !== parent) {
    +		throw new DOMException(NOT_FOUND_ERR, 'child not in parent');
    +	}
    +	if (
    +		!hasInsertableNodeType(node) ||
    +		// the sax parser currently adds top level text nodes, this will be fixed in 0.9.0
    +		// || (node.nodeType === Node.TEXT_NODE && parent.nodeType === Node.DOCUMENT_NODE)
    +		(isDocTypeNode(node) && parent.nodeType !== Node.DOCUMENT_NODE)
    +	) {
    +		throw new DOMException(
    +			HIERARCHY_REQUEST_ERR,
    +			'Unexpected node type ' + node.nodeType + ' for parent node type ' + parent.nodeType
    +		);
    +	}
    +	var parentChildNodes = parent.childNodes || [];
    +	var nodeChildNodes = node.childNodes || [];
    +	if (parent.nodeType === Node.DOCUMENT_NODE) {
    +		if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
    +			let nodeChildElements = nodeChildNodes.filter(isElementNode);
    +			if (nodeChildElements.length > 1 || nodeChildNodes.find(isTextNode)) {
    +				throw new DOMException(HIERARCHY_REQUEST_ERR, 'More than one element or text in fragment');
    +			}
    +			if (nodeChildElements.length === 1 && !isElementInsertionPossible(parent, child)) {
    +				throw new DOMException(HIERARCHY_REQUEST_ERR, 'Element in fragment can not be inserted before doctype');
    +			}
    +		}
    +		if (isElementNode(node)) {
    +			if (parentChildNodes.find(isElementNode) || !isElementInsertionPossible(parent, child)) {
    +				throw new DOMException(HIERARCHY_REQUEST_ERR, 'Only one element can be added and only after doctype');
    +			}
    +		}
    +		if (isDocTypeNode(node)) {
    +			if (parentChildNodes.find(isDocTypeNode)) {
    +				throw new DOMException(HIERARCHY_REQUEST_ERR, 'Only one doctype is allowed');
    +			}
    +			let parentElementChild = parentChildNodes.find(isElementNode);
    +			if (child && parentChildNodes.indexOf(parentElementChild) < parentChildNodes.indexOf(child)) {
    +				throw new DOMException(HIERARCHY_REQUEST_ERR, 'Doctype can only be inserted before an element');
    +			}
    +			if (!child && parentElementChild) {
    +				throw new DOMException(HIERARCHY_REQUEST_ERR, 'Doctype can not be appended since element is present');
    +			}
    +		}
    +	}
    +
    +	var cp = node.parentNode;
     	if (cp) {
    -		cp.removeChild(newChild); //remove and update
    +		cp.removeChild(node); //remove and update
     	}
    -	if (newChild.nodeType === DOCUMENT_FRAGMENT_NODE) {
    -		var newFirst = newChild.firstChild;
    +	if (node.nodeType === DOCUMENT_FRAGMENT_NODE) {
    +		var newFirst = node.firstChild;
     		if (newFirst == null) {
    -			return newChild;
    +			return node;
     		}
    -		var newLast = newChild.lastChild;
    +		var newLast = node.lastChild;
     	} else {
    -		newFirst = newLast = newChild;
    +		newFirst = newLast = node;
     	}
    -	var pre = nextChild ? nextChild.previousSibling : parentNode.lastChild;
    +	var pre = child ? child.previousSibling : parent.lastChild;
     
     	newFirst.previousSibling = pre;
    -	newLast.nextSibling = nextChild;
    +	newLast.nextSibling = child;
     
     	if (pre) {
     		pre.nextSibling = newFirst;
     	} else {
    -		parentNode.firstChild = newFirst;
    +		parent.firstChild = newFirst;
     	}
    -	if (nextChild == null) {
    -		parentNode.lastChild = newLast;
    +	if (child == null) {
    +		parent.lastChild = newLast;
     	} else {
    -		nextChild.previousSibling = newLast;
    +		child.previousSibling = newLast;
     	}
     	do {
    -		newFirst.parentNode = parentNode;
    +		newFirst.parentNode = parent;
     	} while (newFirst !== newLast && (newFirst = newFirst.nextSibling));
    -	_onUpdateChild(parentNode.ownerDocument || parentNode, parentNode);
    -	//console.log(parentNode.lastChild.nextSibling == null)
    -	if (newChild.nodeType == DOCUMENT_FRAGMENT_NODE) {
    -		newChild.firstChild = newChild.lastChild = null;
    +	_onUpdateChild(parent.ownerDocument || parent, parent);
    +	//console.log(parent.lastChild.nextSibling == null)
    +	if (node.nodeType == DOCUMENT_FRAGMENT_NODE) {
    +		node.firstChild = node.lastChild = null;
     	}
    -	return newChild;
    +	return node;
     }
     
     /**
    @@ -840,11 +994,13 @@ Document.prototype = {
     			}
     			return newChild;
     		}
    -		if (this.documentElement == null && newChild.nodeType == ELEMENT_NODE) {
    +		_insertBefore(this, newChild, refChild);
    +		newChild.ownerDocument = this;
    +		if (this.documentElement === null && newChild.nodeType === ELEMENT_NODE) {
     			this.documentElement = newChild;
     		}
     
    -		return _insertBefore(this, newChild, refChild), (newChild.ownerDocument = this), newChild;
    +		return newChild;
     	},
     	removeChild: function (oldChild) {
     		if (this.documentElement == oldChild) {
    
  • test/dom/document.test.js+70 6 modified
    @@ -1,16 +1,16 @@
     'use strict';
     
     const { getTestParser } = require('../get-test-parser');
    -const { DOMImplementation } = require('../../lib/dom');
    +const { DOMImplementation, DOMException } = require('../../lib/dom');
     const { NAMESPACE } = require('../../lib/conventions');
     
     const INPUT = (first = '', second = '', third = '', fourth = '') => `
     <html >
    -	<body id="body">
    -		<p id="p1" class=" quote first   odd ${first} ">Lorem ipsum</p>
    -		<p id="p2" class=" quote second  even ${second} ">Lorem ipsum</p>
    -		<p id="p3" class=" quote third   odd ${third} ">Lorem ipsum</p>
    -		<p id="p4" class=" quote fourth  even ${fourth} ">Lorem ipsum</p>
    +	<body id='body'>
    +		<p id='p1' class=' quote first   odd ${first} '>Lorem ipsum</p>
    +		<p id='p2' class=' quote second  even ${second} '>Lorem ipsum</p>
    +		<p id='p3' class=' quote third   odd ${third} '>Lorem ipsum</p>
    +		<p id='p4' class=' quote fourth  even ${fourth} '>Lorem ipsum</p>
     	</body>
     </html>
     `;
    @@ -166,4 +166,68 @@ describe('Document.prototype', () => {
     			expect(attr.nodeName).toBe('name');
     		});
     	});
    +	describe('insertBefore', () => {
    +		it('should insert the first element and set `documentElement`', () => {
    +			const doc = new DOMImplementation().createDocument(null, '');
    +			expect(doc.childNodes).toHaveLength(0);
    +			expect(doc.documentElement).toBeNull();
    +			const root = doc.createElement('root');
    +			doc.insertBefore(root);
    +			expect(doc.documentElement).toBe(root);
    +			expect(doc.childNodes).toHaveLength(1);
    +			expect(doc.childNodes.item(0)).toBe(root);
    +		});
    +		it('should prevent inserting a second element', () => {
    +			const doc = new DOMImplementation().createDocument(null, '');
    +			const root = doc.createElement('root');
    +			const second = doc.createElement('second');
    +			doc.insertBefore(root);
    +			expect(() => doc.insertBefore(second)).toThrow(DOMException);
    +			expect(doc.documentElement).toBe(root);
    +			expect(doc.childNodes).toHaveLength(1);
    +		});
    +		it('should prevent inserting an element before a doctype', () => {
    +			const impl = new DOMImplementation();
    +			const doctype = impl.createDocumentType('DT');
    +			const doc = impl.createDocument(null, '', doctype);
    +			expect(doc.childNodes).toHaveLength(1);
    +			const root = doc.createElement('root');
    +			expect(() => doc.insertBefore(root, doctype)).toThrow(DOMException);
    +			expect(doc.documentElement).toBeNull();
    +			expect(doc.childNodes).toHaveLength(1);
    +			expect(root.parentNode).toBeNull();
    +		});
    +		it('should prevent inserting a second doctype', () => {
    +			const impl = new DOMImplementation();
    +			const doctype = impl.createDocumentType('DT');
    +			const doctype2 = impl.createDocumentType('DT2');
    +			const doc = impl.createDocument(null, '', doctype);
    +			expect(doc.childNodes).toHaveLength(1);
    +			expect(() => doc.insertBefore(doctype2)).toThrow(DOMException);
    +			expect(doc.childNodes).toHaveLength(1);
    +		});
    +		it('should prevent inserting a doctype after an element', () => {
    +			const impl = new DOMImplementation();
    +			const doc = impl.createDocument(null, '');
    +			const root = doc.createElement('root');
    +			doc.insertBefore(root);
    +			const doctype = impl.createDocumentType('DT');
    +			expect(doc.childNodes).toHaveLength(1);
    +
    +			expect(() => doc.insertBefore(doctype)).toThrow(DOMException);
    +
    +			expect(doc.childNodes).toHaveLength(1);
    +		});
    +		it('should prevent inserting before an child which is not a child of parent', () => {
    +			const doc = new DOMImplementation().createDocument(null, '');
    +			const root = doc.createElement('root');
    +			const withoutParent = doc.createElement('second');
    +
    +			expect(() => doc.insertBefore(root, withoutParent)).toThrow(DOMException);
    +
    +			expect(doc.documentElement).toBeNull();
    +			expect(doc.childNodes).toHaveLength(0);
    +			expect(root.parentNode).toBeNull();
    +		});
    +	});
     });
    
  • test/dom/dom-implementation.test.js+2 0 modified
    @@ -29,6 +29,7 @@ describe('DOMImplementation', () => {
     
     			expect(doc.nodeType).toBe(Node.DOCUMENT_NODE);
     			expect(doc.implementation).toBe(impl);
    +			expect(doc.ownerDocument).toBe(doc);
     			expect(doc.doctype).toBe(null);
     			expect(doc.childNodes).toBeInstanceOf(NodeList);
     			expect(doc.documentElement).toBe(null);
    @@ -193,6 +194,7 @@ describe('DOMImplementation', () => {
     			expect(doc.childNodes.length).toBe(0);
     			expect(doc.doctype).toBeNull();
     			expect(doc.documentElement).toBeNull();
    +			expect(doc.ownerDocument).toBe(doc);
     		});
     		it('should create an HTML document with minimum specified elements when title not provided', () => {
     			const impl = new DOMImplementation();
    
  • test/parse/parse-element.test.js+2 2 modified
    @@ -35,8 +35,8 @@ describe('XML Node Parse', () => {
     	});
     
     	it('sibling closing tag with whitespace', () => {
    -		const actual = new DOMParser().parseFromString(`<book></book ><title>Harry Potter</title>`, 'text/xml').toString();
    -		expect(actual).toBe(`<book/><title>Harry Potter</title>`);
    +		const actual = new DOMParser().parseFromString(`<xml><book></book ><title>Harry Potter</title></xml>`, 'text/xml').toString();
    +		expect(actual).toBe(`<xml><book/><title>Harry Potter</title></xml>`);
     	});
     
     	describe('simple attributes', () => {
    
  • test/xmltest/__snapshots__/not-wf.test.js.snap+16 3 modified
    @@ -315,14 +315,22 @@ Object {
     exports[`xmltest/not-wellformed standalone should match 040.xml with snapshot 1`] = `
     Object {
       "actual": "<doc/>
    -<doc/>",
    +&lt;doc&gt;",
    +  "error": Array [
    +    "[xmldom error]	element parse error: Error: Hierarchy request error: Only one element can be added and only after doctype
    +@#[line:2,col:1]",
    +  ],
     }
     `;
     
     exports[`xmltest/not-wellformed standalone should match 041.xml with snapshot 1`] = `
     Object {
       "actual": "<doc/>
    -<doc/>",
    +&lt;doc&gt;",
    +  "error": Array [
    +    "[xmldom error]	element parse error: Error: Hierarchy request error: Only one element can be added and only after doctype
    +@#[line:2,col:1]",
    +  ],
     }
     `;
     
    @@ -342,7 +350,12 @@ Illegal data
     
     exports[`xmltest/not-wellformed standalone should match 044.xml with snapshot 1`] = `
     Object {
    -  "actual": "<doc/><doc/>",
    +  "actual": "<doc/>&lt;doc/&gt;
    +",
    +  "error": Array [
    +    "[xmldom error]	element parse error: Error: Hierarchy request error: Only one element can be added and only after doctype
    +@#[line:1,col:7]",
    +  ],
     }
     `;
     
    
c02f786216be

Merge pull request from GHSA-crh6-fp67-6883

https://github.com/xmldom/xmldomChristian BewernitzOct 29, 2022via ghsa
4 files changed · +292 61
  • lib/dom.js+205 49 modified
    @@ -158,14 +158,14 @@ NodeList.prototype = {
     	 * The number of nodes in the list. The range of valid child node indices is 0 to length-1 inclusive.
     	 * @standard level1
     	 */
    -	length:0, 
    +	length:0,
     	/**
     	 * Returns the indexth item in the collection. If index is greater than or equal to the number of nodes in the list, this returns null.
     	 * @standard level1
    -	 * @param index  unsigned long 
    +	 * @param index  unsigned long
     	 *   Index into the collection.
     	 * @return Node
    -	 * 	The node at the indexth position in the NodeList, or null if that is not a valid index. 
    +	 * 	The node at the indexth position in the NodeList, or null if that is not a valid index.
     	 */
     	item: function(index) {
     		return this[index] || null;
    @@ -175,7 +175,31 @@ NodeList.prototype = {
     			serializeToString(this[i],buf,isHTML,nodeFilter);
     		}
     		return buf.join('');
    -	}
    +	},
    +	/**
    +	 * @private
    +	 * @param {function (Node):boolean} predicate
    +	 * @returns {Node | undefined}
    +	 */
    +	find: function (predicate) {
    +		return Array.prototype.find.call(this, predicate);
    +	},
    +	/**
    +	 * @private
    +	 * @param {function (Node):boolean} predicate
    +	 * @returns {Node[]}
    +	 */
    +	filter: function (predicate) {
    +		return Array.prototype.filter.call(this, predicate);
    +	},
    +	/**
    +	 * @private
    +	 * @param {Node} item
    +	 * @returns {number}
    +	 */
    +	indexOf: function (item) {
    +		return Array.prototype.indexOf.call(this, item);
    +	},
     };
     
     function LiveNodeList(node,refresh){
    @@ -209,7 +233,7 @@ _extends(LiveNodeList,NodeList);
      * but this is simply to allow convenient enumeration of the contents of a NamedNodeMap,
      * and does not imply that the DOM specifies an order to these Nodes.
      * NamedNodeMap objects in the DOM are live.
    - * used for attributes or DocumentType entities 
    + * used for attributes or DocumentType entities
      */
     function NamedNodeMap() {
     };
    @@ -253,7 +277,7 @@ function _removeNamedNode(el,list,attr){
     			}
     		}
     	}else{
    -		throw DOMException(NOT_FOUND_ERR,new Error(el.tagName+'@'+attr))
    +		throw new DOMException(NOT_FOUND_ERR,new Error(el.tagName+'@'+attr))
     	}
     }
     NamedNodeMap.prototype = {
    @@ -298,10 +322,10 @@ NamedNodeMap.prototype = {
     		var attr = this.getNamedItem(key);
     		_removeNamedNode(this._ownerElement,this,attr);
     		return attr;
    -		
    -		
    +
    +
     	},// raises: NOT_FOUND_ERR,NO_MODIFICATION_ALLOWED_ERR
    -	
    +
     	//for level2
     	removeNamedItemNS:function(namespaceURI,localName){
     		var attr = this.getNamedItemNS(namespaceURI,localName);
    @@ -447,10 +471,10 @@ Node.prototype = {
     	prefix : null,
     	localName : null,
     	// Modified in DOM Level 2:
    -	insertBefore:function(newChild, refChild){//raises 
    +	insertBefore:function(newChild, refChild){//raises
     		return _insertBefore(this,newChild,refChild);
     	},
    -	replaceChild:function(newChild, oldChild){//raises 
    +	replaceChild:function(newChild, oldChild){//raises
     		this.insertBefore(newChild,oldChild);
     		if(oldChild){
     			this.removeChild(oldChild);
    @@ -618,7 +642,7 @@ function _onUpdateChild(doc,el,newChild){
     /**
      * attributes;
      * children;
    - * 
    + *
      * writeable properties:
      * nodeValue,Attr:value,CharacterData:data
      * prefix
    @@ -639,48 +663,177 @@ function _removeChild(parentNode,child){
     	_onUpdateChild(parentNode.ownerDocument,parentNode);
     	return child;
     }
    +
     /**
    - * preformance key(refChild == null)
    + * Returns `true` if `node` can be a parent for insertion.
    + * @param {Node} node
    + * @returns {boolean}
      */
    -function _insertBefore(parentNode,newChild,nextChild){
    -	var cp = newChild.parentNode;
    +function hasValidParentNodeType(node) {
    +	return (
    +		node &&
    +		(node.nodeType === Node.DOCUMENT_NODE || node.nodeType === Node.DOCUMENT_FRAGMENT_NODE || node.nodeType === Node.ELEMENT_NODE)
    +	);
    +}
    +
    +/**
    + * Returns `true` if `node` can be inserted according to it's `nodeType`.
    + * @param {Node} node
    + * @returns {boolean}
    + */
    +function hasInsertableNodeType(node) {
    +	return (
    +		node &&
    +		(isElementNode(node) ||
    +			isTextNode(node) ||
    +			isDocTypeNode(node) ||
    +			node.nodeType === Node.DOCUMENT_FRAGMENT_NODE ||
    +			node.nodeType === Node.COMMENT_NODE ||
    +			node.nodeType === Node.PROCESSING_INSTRUCTION_NODE)
    +	);
    +}
    +
    +/**
    + * Returns true if `node` is a DOCTYPE node
    + * @param {Node} node
    + * @returns {boolean}
    + */
    +function isDocTypeNode(node) {
    +	return node && node.nodeType === Node.DOCUMENT_TYPE_NODE;
    +}
    +
    +/**
    + * Returns true if the node is an element
    + * @param {Node} node
    + * @returns {boolean}
    + */
    +function isElementNode(node) {
    +	return node && node.nodeType === Node.ELEMENT_NODE;
    +}
    +/**
    + * Returns true if `node` is a text node
    + * @param {Node} node
    + * @returns {boolean}
    + */
    +function isTextNode(node) {
    +	return node && node.nodeType === Node.TEXT_NODE;
    +}
    +
    +/**
    + * Check if en element node can be inserted before `child`, or at the end if child is falsy,
    + * according to the presence and position of a doctype node on the same level.
    + *
    + * @param {Document} doc The document node
    + * @param {Node} child the node that would become the nextSibling if the element would be inserted
    + * @returns {boolean} `true` if an element can be inserted before child
    + * @private
    + * https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity
    + */
    +function isElementInsertionPossible(doc, child) {
    +	var parentChildNodes = doc.childNodes || [];
    +	if (parentChildNodes.find(isElementNode) || isDocTypeNode(child)) {
    +		return false;
    +	}
    +	var docTypeNode = parentChildNodes.find(isDocTypeNode);
    +	return !(child && docTypeNode && parentChildNodes.indexOf(docTypeNode) > parentChildNodes.indexOf(child));
    +}
    +/**
    + * @private
    + * @param {Node} parent the parent node to insert `node` into
    + * @param {Node} node the node to insert
    + * @param {Node=} child the node that should become the `nextSibling` of `node`
    + * @returns {Node}
    + * @throws DOMException for several node combinations that would create a DOM that is not well-formed.
    + * @throws DOMException if `child` is provided but is not a child of `parent`.
    + * @see https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity
    + */
    +function _insertBefore(parent, node, child) {
    +	if (!hasValidParentNodeType(parent)) {
    +		throw new DOMException(HIERARCHY_REQUEST_ERR, 'Unexpected parent node type ' + parent.nodeType);
    +	}
    +	if (child && child.parentNode !== parent) {
    +		throw new DOMException(NOT_FOUND_ERR, 'child not in parent');
    +	}
    +	if (
    +		!hasInsertableNodeType(node) ||
    +		// the sax parser currently adds top level text nodes, this will be fixed in 0.9.0
    +		// || (node.nodeType === Node.TEXT_NODE && parent.nodeType === Node.DOCUMENT_NODE)
    +		(isDocTypeNode(node) && parent.nodeType !== Node.DOCUMENT_NODE)
    +	) {
    +		throw new DOMException(
    +			HIERARCHY_REQUEST_ERR,
    +			'Unexpected node type ' + node.nodeType + ' for parent node type ' + parent.nodeType
    +		);
    +	}
    +	var parentChildNodes = parent.childNodes || [];
    +	var nodeChildNodes = node.childNodes || [];
    +	if (parent.nodeType === Node.DOCUMENT_NODE) {
    +		if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
    +			let nodeChildElements = nodeChildNodes.filter(isElementNode);
    +			if (nodeChildElements.length > 1 || nodeChildNodes.find(isTextNode)) {
    +				throw new DOMException(HIERARCHY_REQUEST_ERR, 'More than one element or text in fragment');
    +			}
    +			if (nodeChildElements.length === 1 && !isElementInsertionPossible(parent, child)) {
    +				throw new DOMException(HIERARCHY_REQUEST_ERR, 'Element in fragment can not be inserted before doctype');
    +			}
    +		}
    +		if (isElementNode(node)) {
    +			if (parentChildNodes.find(isElementNode) || !isElementInsertionPossible(parent, child)) {
    +				throw new DOMException(HIERARCHY_REQUEST_ERR, 'Only one element can be added and only after doctype');
    +			}
    +		}
    +		if (isDocTypeNode(node)) {
    +			if (parentChildNodes.find(isDocTypeNode)) {
    +				throw new DOMException(HIERARCHY_REQUEST_ERR, 'Only one doctype is allowed');
    +			}
    +			let parentElementChild = parentChildNodes.find(isElementNode);
    +			if (child && parentChildNodes.indexOf(parentElementChild) < parentChildNodes.indexOf(child)) {
    +				throw new DOMException(HIERARCHY_REQUEST_ERR, 'Doctype can only be inserted before an element');
    +			}
    +			if (!child && parentElementChild) {
    +				throw new DOMException(HIERARCHY_REQUEST_ERR, 'Doctype can not be appended since element is present');
    +			}
    +		}
    +	}
    +
    +	var cp = node.parentNode;
     	if(cp){
    -		cp.removeChild(newChild);//remove and update
    +		cp.removeChild(node);//remove and update
     	}
    -	if(newChild.nodeType === DOCUMENT_FRAGMENT_NODE){
    -		var newFirst = newChild.firstChild;
    +	if(node.nodeType === DOCUMENT_FRAGMENT_NODE){
    +		var newFirst = node.firstChild;
     		if (newFirst == null) {
    -			return newChild;
    +			return node;
     		}
    -		var newLast = newChild.lastChild;
    +		var newLast = node.lastChild;
     	}else{
    -		newFirst = newLast = newChild;
    +		newFirst = newLast = node;
     	}
    -	var pre = nextChild ? nextChild.previousSibling : parentNode.lastChild;
    +	var pre = child ? child.previousSibling : parent.lastChild;
     
     	newFirst.previousSibling = pre;
    -	newLast.nextSibling = nextChild;
    -	
    -	
    +	newLast.nextSibling = child;
    +
    +
     	if(pre){
     		pre.nextSibling = newFirst;
     	}else{
    -		parentNode.firstChild = newFirst;
    +		parent.firstChild = newFirst;
     	}
    -	if(nextChild == null){
    -		parentNode.lastChild = newLast;
    +	if(child == null){
    +		parent.lastChild = newLast;
     	}else{
    -		nextChild.previousSibling = newLast;
    +		child.previousSibling = newLast;
     	}
     	do{
    -		newFirst.parentNode = parentNode;
    +		newFirst.parentNode = parent;
     	}while(newFirst !== newLast && (newFirst= newFirst.nextSibling))
    -	_onUpdateChild(parentNode.ownerDocument||parentNode,parentNode);
    -	//console.log(parentNode.lastChild.nextSibling == null)
    -	if (newChild.nodeType == DOCUMENT_FRAGMENT_NODE) {
    -		newChild.firstChild = newChild.lastChild = null;
    +	_onUpdateChild(parent.ownerDocument||parent, parent);
    +	//console.log(parent.lastChild.nextSibling == null)
    +	if (node.nodeType == DOCUMENT_FRAGMENT_NODE) {
    +		node.firstChild = node.lastChild = null;
     	}
    -	return newChild;
    +	return node;
     }
     function _appendSingleChild(parentNode,newChild){
     	var cp = newChild.parentNode;
    @@ -703,6 +856,7 @@ function _appendSingleChild(parentNode,newChild){
     	return newChild;
     	//console.log("__aa",parentNode.lastChild.nextSibling == null)
     }
    +
     Document.prototype = {
     	//implementation : null,
     	nodeName :  '#document',
    @@ -727,11 +881,13 @@ Document.prototype = {
     			}
     			return newChild;
     		}
    -		if(this.documentElement == null && newChild.nodeType == ELEMENT_NODE){
    +		_insertBefore(this, newChild, refChild);
    +		newChild.ownerDocument = this;
    +		if (this.documentElement === null && newChild.nodeType === ELEMENT_NODE) {
     			this.documentElement = newChild;
     		}
     
    -		return _insertBefore(this,newChild,refChild),(newChild.ownerDocument = this),newChild;
    +		return newChild;
     	},
     	removeChild :  function(oldChild){
     		if(this.documentElement == oldChild){
    @@ -925,7 +1081,7 @@ Element.prototype = {
     		var attr = this.getAttributeNode(name)
     		attr && this.removeAttributeNode(attr);
     	},
    -	
    +
     	//four real opeartion method
     	appendChild:function(newChild){
     		if(newChild.nodeType === DOCUMENT_FRAGMENT_NODE){
    @@ -949,7 +1105,7 @@ Element.prototype = {
     		var old = this.getAttributeNodeNS(namespaceURI, localName);
     		old && this.removeAttributeNode(old);
     	},
    -	
    +
     	hasAttributeNS : function(namespaceURI, localName){
     		return this.getAttributeNodeNS(namespaceURI, localName)!=null;
     	},
    @@ -965,7 +1121,7 @@ Element.prototype = {
     	getAttributeNodeNS : function(namespaceURI, localName){
     		return this.attributes.getNamedItemNS(namespaceURI, localName);
     	},
    -	
    +
     	getElementsByTagName : function(tagName){
     		return new LiveNodeList(this,function(base){
     			var ls = [];
    @@ -986,7 +1142,7 @@ Element.prototype = {
     				}
     			});
     			return ls;
    -			
    +
     		});
     	}
     };
    @@ -1015,7 +1171,7 @@ CharacterData.prototype = {
     	},
     	insertData: function(offset,text) {
     		this.replaceData(offset,0,text);
    -	
    +
     	},
     	appendChild:function(newChild){
     		throw new Error(ExceptionMessage[HIERARCHY_REQUEST_ERR])
    @@ -1109,7 +1265,7 @@ function nodeSerializeToString(isHtml,nodeFilter){
     	var refNode = this.nodeType == 9 && this.documentElement || this;
     	var prefix = refNode.prefix;
     	var uri = refNode.namespaceURI;
    -	
    +
     	if(uri && prefix == null){
     		//console.log(prefix)
     		var prefix = refNode.lookupPrefix(uri);
    @@ -1142,8 +1298,8 @@ function needNamespaceDefine(node, isHTML, visibleNamespaces) {
     	if (prefix === "xml" && uri === NAMESPACE.XML || uri === NAMESPACE.XMLNS) {
     		return false;
     	}
    -	
    -	var i = visibleNamespaces.length 
    +
    +	var i = visibleNamespaces.length
     	while (i--) {
     		var ns = visibleNamespaces[i];
     		// get namespace prefix
    @@ -1187,7 +1343,7 @@ function serializeToString(node,buf,isHTML,nodeFilter,visibleNamespaces){
     		var len = attrs.length;
     		var child = node.firstChild;
     		var nodeName = node.tagName;
    -		
    +
     		isHTML = NAMESPACE.isHTML(node.namespaceURI) || isHTML
     
     		var prefixedNodeName = nodeName
    @@ -1246,14 +1402,14 @@ function serializeToString(node,buf,isHTML,nodeFilter,visibleNamespaces){
     			serializeToString(attr,buf,isHTML,nodeFilter,visibleNamespaces);
     		}
     
    -		// add namespace for current node		
    +		// add namespace for current node
     		if (nodeName === prefixedNodeName && needNamespaceDefine(node, isHTML, visibleNamespaces)) {
     			var prefix = node.prefix||'';
     			var uri = node.namespaceURI;
     			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('>');
     			//if is cdata child node
    @@ -1468,7 +1624,7 @@ try{
     				}
     			}
     		})
    -		
    +
     		function getTextContent(node){
     			switch(node.nodeType){
     			case ELEMENT_NODE:
    
  • test/dom/document.test.js+67 3 modified
    @@ -1,7 +1,7 @@
    -'use strict'
    +'use strict';
     
    -const { getTestParser } = require('../get-test-parser')
    -const { DOMImplementation } = require('../../lib/dom')
    +const { getTestParser } = require('../get-test-parser');
    +const { DOMImplementation, DOMException } = require('../../lib/dom');
     
     const INPUT = (first = '', second = '', third = '', fourth = '') => `
     <html >
    @@ -83,4 +83,68 @@ describe('Document.prototype', () => {
     			expect(doc.firstChild === doctype).toBe(true)
     		})
     	})
    +	describe('insertBefore', () => {
    +		it('should insert the first element and set `documentElement`', () => {
    +			const doc = new DOMImplementation().createDocument(null, '');
    +			expect(doc.childNodes).toHaveLength(0);
    +			expect(doc.documentElement).toBeNull();
    +			const root = doc.createElement('root');
    +			doc.insertBefore(root);
    +			expect(doc.documentElement).toBe(root);
    +			expect(doc.childNodes).toHaveLength(1);
    +			expect(doc.childNodes.item(0)).toBe(root);
    +		});
    +		it('should prevent inserting a second element', () => {
    +			const doc = new DOMImplementation().createDocument(null, '');
    +			const root = doc.createElement('root');
    +			const second = doc.createElement('second');
    +			doc.insertBefore(root);
    +			expect(() => doc.insertBefore(second)).toThrow(DOMException);
    +			expect(doc.documentElement).toBe(root);
    +			expect(doc.childNodes).toHaveLength(1);
    +		});
    +		it('should prevent inserting an element before a doctype', () => {
    +			const impl = new DOMImplementation();
    +			const doctype = impl.createDocumentType('DT');
    +			const doc = impl.createDocument(null, '', doctype);
    +			expect(doc.childNodes).toHaveLength(1);
    +			const root = doc.createElement('root');
    +			expect(() => doc.insertBefore(root, doctype)).toThrow(DOMException);
    +			expect(doc.documentElement).toBeNull();
    +			expect(doc.childNodes).toHaveLength(1);
    +			expect(root.parentNode).toBeNull();
    +		});
    +		it('should prevent inserting a second doctype', () => {
    +			const impl = new DOMImplementation();
    +			const doctype = impl.createDocumentType('DT');
    +			const doctype2 = impl.createDocumentType('DT2');
    +			const doc = impl.createDocument(null, '', doctype);
    +			expect(doc.childNodes).toHaveLength(1);
    +			expect(() => doc.insertBefore(doctype2)).toThrow(DOMException);
    +			expect(doc.childNodes).toHaveLength(1);
    +		});
    +		it('should prevent inserting a doctype after an element', () => {
    +			const impl = new DOMImplementation();
    +			const doc = impl.createDocument(null, '');
    +			const root = doc.createElement('root');
    +			doc.insertBefore(root);
    +			const doctype = impl.createDocumentType('DT');
    +			expect(doc.childNodes).toHaveLength(1);
    +
    +			expect(() => doc.insertBefore(doctype)).toThrow(DOMException);
    +
    +			expect(doc.childNodes).toHaveLength(1);
    +		});
    +		it('should prevent inserting before an child which is not a child of parent', () => {
    +			const doc = new DOMImplementation().createDocument(null, '');
    +			const root = doc.createElement('root');
    +			const withoutParent = doc.createElement('second');
    +
    +			expect(() => doc.insertBefore(root, withoutParent)).toThrow(DOMException);
    +
    +			expect(doc.documentElement).toBeNull();
    +			expect(doc.childNodes).toHaveLength(0);
    +			expect(root.parentNode).toBeNull();
    +		});
    +	});
     })
    
  • test/parse/parse-element.test.js+4 6 modified
    @@ -38,12 +38,10 @@ describe('XML Node Parse', () => {
     </bookstore>`)
     	})
     
    -	it('nested closing tag with whitespace', () => {
    -		const actual = new DOMParser()
    -			.parseFromString(`<book></book ><title>Harry Potter</title>`, 'text/xml')
    -			.toString()
    -		expect(actual).toBe(`<book/><title>Harry Potter</title>`)
    -	})
    +	it('sibling closing tag with whitespace', () => {
    +		const actual = new DOMParser().parseFromString(`<xml><book></book ><title>Harry Potter</title></xml>`, 'text/xml').toString();
    +		expect(actual).toBe(`<xml><book/><title>Harry Potter</title></xml>`);
    +	});
     
     	describe('simple attributes', () => {
     		describe('nothing special', () => {
    
  • test/xmltest/__snapshots__/not-wf.test.js.snap+16 3 modified
    @@ -316,14 +316,22 @@ Object {
     exports[`xmltest/not-wellformed standalone should match 040.xml with snapshot 1`] = `
     Object {
       "actual": "<doc/>
    -<doc/>",
    +&lt;doc>",
    +  "error": Array [
    +    "[xmldom error]	element parse error: Error: Hierarchy request error: Only one element can be added and only after doctype
    +@#[line:2,col:1]",
    +  ],
     }
     `;
     
     exports[`xmltest/not-wellformed standalone should match 041.xml with snapshot 1`] = `
     Object {
       "actual": "<doc/>
    -<doc/>",
    +&lt;doc>",
    +  "error": Array [
    +    "[xmldom error]	element parse error: Error: Hierarchy request error: Only one element can be added and only after doctype
    +@#[line:2,col:1]",
    +  ],
     }
     `;
     
    @@ -343,7 +351,12 @@ Illegal data
     
     exports[`xmltest/not-wellformed standalone should match 044.xml with snapshot 1`] = `
     Object {
    -  "actual": "<doc/><doc/>",
    +  "actual": "<doc/>&lt;doc/>
    +",
    +  "error": Array [
    +    "[xmldom error]	element parse error: Error: Hierarchy request error: Only one element can be added and only after doctype
    +@#[line:1,col:7]",
    +  ],
     }
     `;
     
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

11

News mentions

0

No linked articles in our index yet.