@xmldom/xmldom 0.8.3 → 0.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/lib/dom.js +203 -48
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,19 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.8.4](https://github.com/xmldom/xmldom/compare/0.8.3...0.8.4)
8
+
9
+ ### Fixed
10
+
11
+ - 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)
12
+ 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.
13
+ In the upcoming version 0.9.0 those text nodes will no longer be added and an error will be thrown instead.
14
+ 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.
15
+ Related Spec: <https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity>
16
+
17
+ Thank you, [@frumioj](https://github.com/frumioj), [@cjbarth](https://github.com/cjbarth), [@markgollnick](https://github.com/markgollnick) for your contributions
18
+
19
+
7
20
  ## [0.8.3](https://github.com/xmldom/xmldom/compare/0.8.3...0.8.2)
8
21
 
9
22
  ### Fixed
package/lib/dom.js CHANGED
@@ -158,14 +158,14 @@ NodeList.prototype = {
158
158
  * The number of nodes in the list. The range of valid child node indices is 0 to length-1 inclusive.
159
159
  * @standard level1
160
160
  */
161
- length:0,
161
+ length:0,
162
162
  /**
163
163
  * 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.
164
164
  * @standard level1
165
- * @param index unsigned long
165
+ * @param index unsigned long
166
166
  * Index into the collection.
167
167
  * @return Node
168
- * The node at the indexth position in the NodeList, or null if that is not a valid index.
168
+ * The node at the indexth position in the NodeList, or null if that is not a valid index.
169
169
  */
170
170
  item: function(index) {
171
171
  return this[index] || null;
@@ -175,7 +175,31 @@ NodeList.prototype = {
175
175
  serializeToString(this[i],buf,isHTML,nodeFilter);
176
176
  }
177
177
  return buf.join('');
178
- }
178
+ },
179
+ /**
180
+ * @private
181
+ * @param {function (Node):boolean} predicate
182
+ * @returns {Node | undefined}
183
+ */
184
+ find: function (predicate) {
185
+ return Array.prototype.find.call(this, predicate);
186
+ },
187
+ /**
188
+ * @private
189
+ * @param {function (Node):boolean} predicate
190
+ * @returns {Node[]}
191
+ */
192
+ filter: function (predicate) {
193
+ return Array.prototype.filter.call(this, predicate);
194
+ },
195
+ /**
196
+ * @private
197
+ * @param {Node} item
198
+ * @returns {number}
199
+ */
200
+ indexOf: function (item) {
201
+ return Array.prototype.indexOf.call(this, item);
202
+ },
179
203
  };
180
204
 
181
205
  function LiveNodeList(node,refresh){
@@ -209,7 +233,7 @@ _extends(LiveNodeList,NodeList);
209
233
  * but this is simply to allow convenient enumeration of the contents of a NamedNodeMap,
210
234
  * and does not imply that the DOM specifies an order to these Nodes.
211
235
  * NamedNodeMap objects in the DOM are live.
212
- * used for attributes or DocumentType entities
236
+ * used for attributes or DocumentType entities
213
237
  */
214
238
  function NamedNodeMap() {
215
239
  };
@@ -253,7 +277,7 @@ function _removeNamedNode(el,list,attr){
253
277
  }
254
278
  }
255
279
  }else{
256
- throw DOMException(NOT_FOUND_ERR,new Error(el.tagName+'@'+attr))
280
+ throw new DOMException(NOT_FOUND_ERR,new Error(el.tagName+'@'+attr))
257
281
  }
258
282
  }
259
283
  NamedNodeMap.prototype = {
@@ -298,10 +322,10 @@ NamedNodeMap.prototype = {
298
322
  var attr = this.getNamedItem(key);
299
323
  _removeNamedNode(this._ownerElement,this,attr);
300
324
  return attr;
301
-
302
-
325
+
326
+
303
327
  },// raises: NOT_FOUND_ERR,NO_MODIFICATION_ALLOWED_ERR
304
-
328
+
305
329
  //for level2
306
330
  removeNamedItemNS:function(namespaceURI,localName){
307
331
  var attr = this.getNamedItemNS(namespaceURI,localName);
@@ -447,10 +471,10 @@ Node.prototype = {
447
471
  prefix : null,
448
472
  localName : null,
449
473
  // Modified in DOM Level 2:
450
- insertBefore:function(newChild, refChild){//raises
474
+ insertBefore:function(newChild, refChild){//raises
451
475
  return _insertBefore(this,newChild,refChild);
452
476
  },
453
- replaceChild:function(newChild, oldChild){//raises
477
+ replaceChild:function(newChild, oldChild){//raises
454
478
  this.insertBefore(newChild,oldChild);
455
479
  if(oldChild){
456
480
  this.removeChild(oldChild);
@@ -656,48 +680,177 @@ function _removeChild (parentNode, child) {
656
680
  _onUpdateChild(parentNode.ownerDocument, parentNode);
657
681
  return child;
658
682
  }
683
+
684
+ /**
685
+ * Returns `true` if `node` can be a parent for insertion.
686
+ * @param {Node} node
687
+ * @returns {boolean}
688
+ */
689
+ function hasValidParentNodeType(node) {
690
+ return (
691
+ node &&
692
+ (node.nodeType === Node.DOCUMENT_NODE || node.nodeType === Node.DOCUMENT_FRAGMENT_NODE || node.nodeType === Node.ELEMENT_NODE)
693
+ );
694
+ }
695
+
696
+ /**
697
+ * Returns `true` if `node` can be inserted according to it's `nodeType`.
698
+ * @param {Node} node
699
+ * @returns {boolean}
700
+ */
701
+ function hasInsertableNodeType(node) {
702
+ return (
703
+ node &&
704
+ (isElementNode(node) ||
705
+ isTextNode(node) ||
706
+ isDocTypeNode(node) ||
707
+ node.nodeType === Node.DOCUMENT_FRAGMENT_NODE ||
708
+ node.nodeType === Node.COMMENT_NODE ||
709
+ node.nodeType === Node.PROCESSING_INSTRUCTION_NODE)
710
+ );
711
+ }
712
+
713
+ /**
714
+ * Returns true if `node` is a DOCTYPE node
715
+ * @param {Node} node
716
+ * @returns {boolean}
717
+ */
718
+ function isDocTypeNode(node) {
719
+ return node && node.nodeType === Node.DOCUMENT_TYPE_NODE;
720
+ }
721
+
722
+ /**
723
+ * Returns true if the node is an element
724
+ * @param {Node} node
725
+ * @returns {boolean}
726
+ */
727
+ function isElementNode(node) {
728
+ return node && node.nodeType === Node.ELEMENT_NODE;
729
+ }
730
+ /**
731
+ * Returns true if `node` is a text node
732
+ * @param {Node} node
733
+ * @returns {boolean}
734
+ */
735
+ function isTextNode(node) {
736
+ return node && node.nodeType === Node.TEXT_NODE;
737
+ }
738
+
739
+ /**
740
+ * Check if en element node can be inserted before `child`, or at the end if child is falsy,
741
+ * according to the presence and position of a doctype node on the same level.
742
+ *
743
+ * @param {Document} doc The document node
744
+ * @param {Node} child the node that would become the nextSibling if the element would be inserted
745
+ * @returns {boolean} `true` if an element can be inserted before child
746
+ * @private
747
+ * https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity
748
+ */
749
+ function isElementInsertionPossible(doc, child) {
750
+ var parentChildNodes = doc.childNodes || [];
751
+ if (parentChildNodes.find(isElementNode) || isDocTypeNode(child)) {
752
+ return false;
753
+ }
754
+ var docTypeNode = parentChildNodes.find(isDocTypeNode);
755
+ return !(child && docTypeNode && parentChildNodes.indexOf(docTypeNode) > parentChildNodes.indexOf(child));
756
+ }
659
757
  /**
660
- * preformance key(refChild == null)
758
+ * @private
759
+ * @param {Node} parent the parent node to insert `node` into
760
+ * @param {Node} node the node to insert
761
+ * @param {Node=} child the node that should become the `nextSibling` of `node`
762
+ * @returns {Node}
763
+ * @throws DOMException for several node combinations that would create a DOM that is not well-formed.
764
+ * @throws DOMException if `child` is provided but is not a child of `parent`.
765
+ * @see https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity
661
766
  */
662
- function _insertBefore(parentNode,newChild,nextChild){
663
- var cp = newChild.parentNode;
767
+ function _insertBefore(parent, node, child) {
768
+ if (!hasValidParentNodeType(parent)) {
769
+ throw new DOMException(HIERARCHY_REQUEST_ERR, 'Unexpected parent node type ' + parent.nodeType);
770
+ }
771
+ if (child && child.parentNode !== parent) {
772
+ throw new DOMException(NOT_FOUND_ERR, 'child not in parent');
773
+ }
774
+ if (
775
+ !hasInsertableNodeType(node) ||
776
+ // the sax parser currently adds top level text nodes, this will be fixed in 0.9.0
777
+ // || (node.nodeType === Node.TEXT_NODE && parent.nodeType === Node.DOCUMENT_NODE)
778
+ (isDocTypeNode(node) && parent.nodeType !== Node.DOCUMENT_NODE)
779
+ ) {
780
+ throw new DOMException(
781
+ HIERARCHY_REQUEST_ERR,
782
+ 'Unexpected node type ' + node.nodeType + ' for parent node type ' + parent.nodeType
783
+ );
784
+ }
785
+ var parentChildNodes = parent.childNodes || [];
786
+ var nodeChildNodes = node.childNodes || [];
787
+ if (parent.nodeType === Node.DOCUMENT_NODE) {
788
+ if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
789
+ let nodeChildElements = nodeChildNodes.filter(isElementNode);
790
+ if (nodeChildElements.length > 1 || nodeChildNodes.find(isTextNode)) {
791
+ throw new DOMException(HIERARCHY_REQUEST_ERR, 'More than one element or text in fragment');
792
+ }
793
+ if (nodeChildElements.length === 1 && !isElementInsertionPossible(parent, child)) {
794
+ throw new DOMException(HIERARCHY_REQUEST_ERR, 'Element in fragment can not be inserted before doctype');
795
+ }
796
+ }
797
+ if (isElementNode(node)) {
798
+ if (parentChildNodes.find(isElementNode) || !isElementInsertionPossible(parent, child)) {
799
+ throw new DOMException(HIERARCHY_REQUEST_ERR, 'Only one element can be added and only after doctype');
800
+ }
801
+ }
802
+ if (isDocTypeNode(node)) {
803
+ if (parentChildNodes.find(isDocTypeNode)) {
804
+ throw new DOMException(HIERARCHY_REQUEST_ERR, 'Only one doctype is allowed');
805
+ }
806
+ let parentElementChild = parentChildNodes.find(isElementNode);
807
+ if (child && parentChildNodes.indexOf(parentElementChild) < parentChildNodes.indexOf(child)) {
808
+ throw new DOMException(HIERARCHY_REQUEST_ERR, 'Doctype can only be inserted before an element');
809
+ }
810
+ if (!child && parentElementChild) {
811
+ throw new DOMException(HIERARCHY_REQUEST_ERR, 'Doctype can not be appended since element is present');
812
+ }
813
+ }
814
+ }
815
+
816
+ var cp = node.parentNode;
664
817
  if(cp){
665
- cp.removeChild(newChild);//remove and update
818
+ cp.removeChild(node);//remove and update
666
819
  }
667
- if(newChild.nodeType === DOCUMENT_FRAGMENT_NODE){
668
- var newFirst = newChild.firstChild;
820
+ if(node.nodeType === DOCUMENT_FRAGMENT_NODE){
821
+ var newFirst = node.firstChild;
669
822
  if (newFirst == null) {
670
- return newChild;
823
+ return node;
671
824
  }
672
- var newLast = newChild.lastChild;
825
+ var newLast = node.lastChild;
673
826
  }else{
674
- newFirst = newLast = newChild;
827
+ newFirst = newLast = node;
675
828
  }
676
- var pre = nextChild ? nextChild.previousSibling : parentNode.lastChild;
829
+ var pre = child ? child.previousSibling : parent.lastChild;
677
830
 
678
831
  newFirst.previousSibling = pre;
679
- newLast.nextSibling = nextChild;
680
-
681
-
832
+ newLast.nextSibling = child;
833
+
834
+
682
835
  if(pre){
683
836
  pre.nextSibling = newFirst;
684
837
  }else{
685
- parentNode.firstChild = newFirst;
838
+ parent.firstChild = newFirst;
686
839
  }
687
- if(nextChild == null){
688
- parentNode.lastChild = newLast;
840
+ if(child == null){
841
+ parent.lastChild = newLast;
689
842
  }else{
690
- nextChild.previousSibling = newLast;
843
+ child.previousSibling = newLast;
691
844
  }
692
845
  do{
693
- newFirst.parentNode = parentNode;
846
+ newFirst.parentNode = parent;
694
847
  }while(newFirst !== newLast && (newFirst= newFirst.nextSibling))
695
- _onUpdateChild(parentNode.ownerDocument||parentNode,parentNode);
696
- //console.log(parentNode.lastChild.nextSibling == null)
697
- if (newChild.nodeType == DOCUMENT_FRAGMENT_NODE) {
698
- newChild.firstChild = newChild.lastChild = null;
848
+ _onUpdateChild(parent.ownerDocument||parent, parent);
849
+ //console.log(parent.lastChild.nextSibling == null)
850
+ if (node.nodeType == DOCUMENT_FRAGMENT_NODE) {
851
+ node.firstChild = node.lastChild = null;
699
852
  }
700
- return newChild;
853
+ return node;
701
854
  }
702
855
 
703
856
  /**
@@ -752,11 +905,13 @@ Document.prototype = {
752
905
  }
753
906
  return newChild;
754
907
  }
755
- if(this.documentElement == null && newChild.nodeType == ELEMENT_NODE){
908
+ _insertBefore(this, newChild, refChild);
909
+ newChild.ownerDocument = this;
910
+ if (this.documentElement === null && newChild.nodeType === ELEMENT_NODE) {
756
911
  this.documentElement = newChild;
757
912
  }
758
913
 
759
- return _insertBefore(this,newChild,refChild),(newChild.ownerDocument = this),newChild;
914
+ return newChild;
760
915
  },
761
916
  removeChild : function(oldChild){
762
917
  if(this.documentElement == oldChild){
@@ -950,7 +1105,7 @@ Element.prototype = {
950
1105
  var attr = this.getAttributeNode(name)
951
1106
  attr && this.removeAttributeNode(attr);
952
1107
  },
953
-
1108
+
954
1109
  //four real opeartion method
955
1110
  appendChild:function(newChild){
956
1111
  if(newChild.nodeType === DOCUMENT_FRAGMENT_NODE){
@@ -974,7 +1129,7 @@ Element.prototype = {
974
1129
  var old = this.getAttributeNodeNS(namespaceURI, localName);
975
1130
  old && this.removeAttributeNode(old);
976
1131
  },
977
-
1132
+
978
1133
  hasAttributeNS : function(namespaceURI, localName){
979
1134
  return this.getAttributeNodeNS(namespaceURI, localName)!=null;
980
1135
  },
@@ -990,7 +1145,7 @@ Element.prototype = {
990
1145
  getAttributeNodeNS : function(namespaceURI, localName){
991
1146
  return this.attributes.getNamedItemNS(namespaceURI, localName);
992
1147
  },
993
-
1148
+
994
1149
  getElementsByTagName : function(tagName){
995
1150
  return new LiveNodeList(this,function(base){
996
1151
  var ls = [];
@@ -1011,7 +1166,7 @@ Element.prototype = {
1011
1166
  }
1012
1167
  });
1013
1168
  return ls;
1014
-
1169
+
1015
1170
  });
1016
1171
  }
1017
1172
  };
@@ -1040,7 +1195,7 @@ CharacterData.prototype = {
1040
1195
  },
1041
1196
  insertData: function(offset,text) {
1042
1197
  this.replaceData(offset,0,text);
1043
-
1198
+
1044
1199
  },
1045
1200
  appendChild:function(newChild){
1046
1201
  throw new Error(ExceptionMessage[HIERARCHY_REQUEST_ERR])
@@ -1134,7 +1289,7 @@ function nodeSerializeToString(isHtml,nodeFilter){
1134
1289
  var refNode = this.nodeType == 9 && this.documentElement || this;
1135
1290
  var prefix = refNode.prefix;
1136
1291
  var uri = refNode.namespaceURI;
1137
-
1292
+
1138
1293
  if(uri && prefix == null){
1139
1294
  //console.log(prefix)
1140
1295
  var prefix = refNode.lookupPrefix(uri);
@@ -1167,8 +1322,8 @@ function needNamespaceDefine(node, isHTML, visibleNamespaces) {
1167
1322
  if (prefix === "xml" && uri === NAMESPACE.XML || uri === NAMESPACE.XMLNS) {
1168
1323
  return false;
1169
1324
  }
1170
-
1171
- var i = visibleNamespaces.length
1325
+
1326
+ var i = visibleNamespaces.length
1172
1327
  while (i--) {
1173
1328
  var ns = visibleNamespaces[i];
1174
1329
  // get namespace prefix
@@ -1219,7 +1374,7 @@ function serializeToString(node,buf,isHTML,nodeFilter,visibleNamespaces){
1219
1374
  var len = attrs.length;
1220
1375
  var child = node.firstChild;
1221
1376
  var nodeName = node.tagName;
1222
-
1377
+
1223
1378
  isHTML = NAMESPACE.isHTML(node.namespaceURI) || isHTML
1224
1379
 
1225
1380
  var prefixedNodeName = nodeName
@@ -1278,14 +1433,14 @@ function serializeToString(node,buf,isHTML,nodeFilter,visibleNamespaces){
1278
1433
  serializeToString(attr,buf,isHTML,nodeFilter,visibleNamespaces);
1279
1434
  }
1280
1435
 
1281
- // add namespace for current node
1436
+ // add namespace for current node
1282
1437
  if (nodeName === prefixedNodeName && needNamespaceDefine(node, isHTML, visibleNamespaces)) {
1283
1438
  var prefix = node.prefix||'';
1284
1439
  var uri = node.namespaceURI;
1285
1440
  addSerializedAttribute(buf, prefix ? 'xmlns:' + prefix : "xmlns", uri);
1286
1441
  visibleNamespaces.push({ prefix: prefix, namespace:uri });
1287
1442
  }
1288
-
1443
+
1289
1444
  if(child || isHTML && !/^(?:meta|link|img|br|hr|input)$/i.test(nodeName)){
1290
1445
  buf.push('>');
1291
1446
  //if is cdata child node
@@ -1500,7 +1655,7 @@ try{
1500
1655
  }
1501
1656
  }
1502
1657
  })
1503
-
1658
+
1504
1659
  function getTextContent(node){
1505
1660
  switch(node.nodeType){
1506
1661
  case ELEMENT_NODE:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmldom/xmldom",
3
- "version": "0.8.3",
3
+ "version": "0.8.4",
4
4
  "description": "A pure JavaScript W3C standard-based (XML DOM Level 2 Core) DOMParser and XMLSerializer module.",
5
5
  "keywords": [
6
6
  "w3c",