@xmldom/xmldom
Advanced tools
Comparing version 0.8.3 to 0.8.4
@@ -7,2 +7,15 @@ # Changelog | ||
## [0.8.4](https://github.com/xmldom/xmldom/compare/0.8.3...0.8.4) | ||
### Fixed | ||
- Security: Prevent inserting DOM nodes when they are not well-formed [`CVE-2022-39353`](https://github.com/xmldom/xmldom/security/advisories/GHSA-crh6-fp67-6883) | ||
In case such a DOM would be created, the part that is not well-formed will be transformed into text nodes, in which xml specific characters like `<` and `>` are encoded accordingly. | ||
In the upcoming version 0.9.0 those text nodes will no longer be added and an error will be thrown instead. | ||
This change can break your code, if you relied on this behavior, e.g. multiple root elements in the past. We consider it more important to align with the specs that we want to be aligned with, considering the potential security issues that might derive from people not being aware of the difference in behavior. | ||
Related Spec: <https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity> | ||
Thank you, [@frumioj](https://github.com/frumioj), [@cjbarth](https://github.com/cjbarth), [@markgollnick](https://github.com/markgollnick) for your contributions | ||
## [0.8.3](https://github.com/xmldom/xmldom/compare/0.8.3...0.8.2) | ||
@@ -9,0 +22,0 @@ |
251
lib/dom.js
@@ -161,10 +161,10 @@ var conventions = require("./conventions"); | ||
*/ | ||
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. | ||
*/ | ||
@@ -179,3 +179,27 @@ item: function(index) { | ||
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); | ||
}, | ||
}; | ||
@@ -214,3 +238,3 @@ | ||
* NamedNodeMap objects in the DOM are live. | ||
* used for attributes or DocumentType entities | ||
* used for attributes or DocumentType entities | ||
*/ | ||
@@ -259,3 +283,3 @@ function NamedNodeMap() { | ||
}else{ | ||
throw DOMException(NOT_FOUND_ERR,new Error(el.tagName+'@'+attr)) | ||
throw new DOMException(NOT_FOUND_ERR,new Error(el.tagName+'@'+attr)) | ||
} | ||
@@ -305,6 +329,6 @@ } | ||
return attr; | ||
},// raises: NOT_FOUND_ERR,NO_MODIFICATION_ALLOWED_ERR | ||
//for level2 | ||
@@ -455,6 +479,6 @@ removeNamedItemNS:function(namespaceURI,localName){ | ||
// 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); | ||
@@ -665,44 +689,173 @@ if(oldChild){ | ||
} | ||
/** | ||
* 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; | ||
} | ||
@@ -762,7 +915,9 @@ | ||
} | ||
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; | ||
}, | ||
@@ -961,3 +1116,3 @@ removeChild : function(oldChild){ | ||
}, | ||
//four real opeartion method | ||
@@ -986,3 +1141,3 @@ appendChild:function(newChild){ | ||
}, | ||
hasAttributeNS : function(namespaceURI, localName){ | ||
@@ -1003,3 +1158,3 @@ return this.getAttributeNodeNS(namespaceURI, localName)!=null; | ||
}, | ||
getElementsByTagName : function(tagName){ | ||
@@ -1025,3 +1180,3 @@ return new LiveNodeList(this,function(base){ | ||
return ls; | ||
}); | ||
@@ -1055,3 +1210,3 @@ } | ||
this.replaceData(offset,0,text); | ||
}, | ||
@@ -1150,3 +1305,3 @@ appendChild:function(newChild){ | ||
var uri = refNode.namespaceURI; | ||
if(uri && prefix == null){ | ||
@@ -1184,4 +1339,4 @@ //console.log(prefix) | ||
} | ||
var i = visibleNamespaces.length | ||
var i = visibleNamespaces.length | ||
while (i--) { | ||
@@ -1237,3 +1392,3 @@ var ns = visibleNamespaces[i]; | ||
var nodeName = node.tagName; | ||
isHTML = NAMESPACE.isHTML(node.namespaceURI) || isHTML | ||
@@ -1297,3 +1452,3 @@ | ||
// add namespace for current node | ||
// add namespace for current node | ||
if (nodeName === prefixedNodeName && needNamespaceDefine(node, isHTML, visibleNamespaces)) { | ||
@@ -1305,3 +1460,3 @@ var prefix = node.prefix||''; | ||
} | ||
if(child || isHTML && !/^(?:meta|link|img|br|hr|input)$/i.test(nodeName)){ | ||
@@ -1521,3 +1676,3 @@ buf.push('>'); | ||
}) | ||
function getTextContent(node){ | ||
@@ -1524,0 +1679,0 @@ switch(node.nodeType){ |
{ | ||
"name": "@xmldom/xmldom", | ||
"version": "0.8.3", | ||
"version": "0.8.4", | ||
"description": "A pure JavaScript W3C standard-based (XML DOM Level 2 Core) DOMParser and XMLSerializer module.", | ||
@@ -5,0 +5,0 @@ "keywords": [ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
133057
3002