@xmldom/xmldom 0.7.6 → 0.7.7

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 +11 -0
  2. package/lib/dom.js +205 -49
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -3,6 +3,17 @@
3
3
  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
+ ## [0.7.7](https://github.com/xmldom/xmldom/compare/0.7.6...0.7.7)
7
+
8
+ ### Fixed
9
+
10
+ - 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)
11
+ 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.
12
+ In the upcoming version 0.9.0 those text nodes will no longer be added and an error will be thrown instead.
13
+ 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.
14
+ Related Spec: <https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity>
15
+
16
+ Thank you, [@frumioj](https://github.com/frumioj), [@cjbarth](https://github.com/cjbarth), [@markgollnick](https://github.com/markgollnick) for your contributions
6
17
 
7
18
  ## [0.7.6](https://github.com/xmldom/xmldom/compare/0.7.5...0.7.6)
8
19
 
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);
@@ -618,7 +642,7 @@ function _onUpdateChild(doc,el,newChild){
618
642
  /**
619
643
  * attributes;
620
644
  * children;
621
- *
645
+ *
622
646
  * writeable properties:
623
647
  * nodeValue,Attr:value,CharacterData:data
624
648
  * prefix
@@ -639,48 +663,177 @@ function _removeChild(parentNode,child){
639
663
  _onUpdateChild(parentNode.ownerDocument,parentNode);
640
664
  return child;
641
665
  }
666
+
642
667
  /**
643
- * preformance key(refChild == null)
668
+ * Returns `true` if `node` can be a parent for insertion.
669
+ * @param {Node} node
670
+ * @returns {boolean}
644
671
  */
645
- function _insertBefore(parentNode,newChild,nextChild){
646
- var cp = newChild.parentNode;
672
+ function hasValidParentNodeType(node) {
673
+ return (
674
+ node &&
675
+ (node.nodeType === Node.DOCUMENT_NODE || node.nodeType === Node.DOCUMENT_FRAGMENT_NODE || node.nodeType === Node.ELEMENT_NODE)
676
+ );
677
+ }
678
+
679
+ /**
680
+ * Returns `true` if `node` can be inserted according to it's `nodeType`.
681
+ * @param {Node} node
682
+ * @returns {boolean}
683
+ */
684
+ function hasInsertableNodeType(node) {
685
+ return (
686
+ node &&
687
+ (isElementNode(node) ||
688
+ isTextNode(node) ||
689
+ isDocTypeNode(node) ||
690
+ node.nodeType === Node.DOCUMENT_FRAGMENT_NODE ||
691
+ node.nodeType === Node.COMMENT_NODE ||
692
+ node.nodeType === Node.PROCESSING_INSTRUCTION_NODE)
693
+ );
694
+ }
695
+
696
+ /**
697
+ * Returns true if `node` is a DOCTYPE node
698
+ * @param {Node} node
699
+ * @returns {boolean}
700
+ */
701
+ function isDocTypeNode(node) {
702
+ return node && node.nodeType === Node.DOCUMENT_TYPE_NODE;
703
+ }
704
+
705
+ /**
706
+ * Returns true if the node is an element
707
+ * @param {Node} node
708
+ * @returns {boolean}
709
+ */
710
+ function isElementNode(node) {
711
+ return node && node.nodeType === Node.ELEMENT_NODE;
712
+ }
713
+ /**
714
+ * Returns true if `node` is a text node
715
+ * @param {Node} node
716
+ * @returns {boolean}
717
+ */
718
+ function isTextNode(node) {
719
+ return node && node.nodeType === Node.TEXT_NODE;
720
+ }
721
+
722
+ /**
723
+ * Check if en element node can be inserted before `child`, or at the end if child is falsy,
724
+ * according to the presence and position of a doctype node on the same level.
725
+ *
726
+ * @param {Document} doc The document node
727
+ * @param {Node} child the node that would become the nextSibling if the element would be inserted
728
+ * @returns {boolean} `true` if an element can be inserted before child
729
+ * @private
730
+ * https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity
731
+ */
732
+ function isElementInsertionPossible(doc, child) {
733
+ var parentChildNodes = doc.childNodes || [];
734
+ if (parentChildNodes.find(isElementNode) || isDocTypeNode(child)) {
735
+ return false;
736
+ }
737
+ var docTypeNode = parentChildNodes.find(isDocTypeNode);
738
+ return !(child && docTypeNode && parentChildNodes.indexOf(docTypeNode) > parentChildNodes.indexOf(child));
739
+ }
740
+ /**
741
+ * @private
742
+ * @param {Node} parent the parent node to insert `node` into
743
+ * @param {Node} node the node to insert
744
+ * @param {Node=} child the node that should become the `nextSibling` of `node`
745
+ * @returns {Node}
746
+ * @throws DOMException for several node combinations that would create a DOM that is not well-formed.
747
+ * @throws DOMException if `child` is provided but is not a child of `parent`.
748
+ * @see https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity
749
+ */
750
+ function _insertBefore(parent, node, child) {
751
+ if (!hasValidParentNodeType(parent)) {
752
+ throw new DOMException(HIERARCHY_REQUEST_ERR, 'Unexpected parent node type ' + parent.nodeType);
753
+ }
754
+ if (child && child.parentNode !== parent) {
755
+ throw new DOMException(NOT_FOUND_ERR, 'child not in parent');
756
+ }
757
+ if (
758
+ !hasInsertableNodeType(node) ||
759
+ // the sax parser currently adds top level text nodes, this will be fixed in 0.9.0
760
+ // || (node.nodeType === Node.TEXT_NODE && parent.nodeType === Node.DOCUMENT_NODE)
761
+ (isDocTypeNode(node) && parent.nodeType !== Node.DOCUMENT_NODE)
762
+ ) {
763
+ throw new DOMException(
764
+ HIERARCHY_REQUEST_ERR,
765
+ 'Unexpected node type ' + node.nodeType + ' for parent node type ' + parent.nodeType
766
+ );
767
+ }
768
+ var parentChildNodes = parent.childNodes || [];
769
+ var nodeChildNodes = node.childNodes || [];
770
+ if (parent.nodeType === Node.DOCUMENT_NODE) {
771
+ if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
772
+ let nodeChildElements = nodeChildNodes.filter(isElementNode);
773
+ if (nodeChildElements.length > 1 || nodeChildNodes.find(isTextNode)) {
774
+ throw new DOMException(HIERARCHY_REQUEST_ERR, 'More than one element or text in fragment');
775
+ }
776
+ if (nodeChildElements.length === 1 && !isElementInsertionPossible(parent, child)) {
777
+ throw new DOMException(HIERARCHY_REQUEST_ERR, 'Element in fragment can not be inserted before doctype');
778
+ }
779
+ }
780
+ if (isElementNode(node)) {
781
+ if (parentChildNodes.find(isElementNode) || !isElementInsertionPossible(parent, child)) {
782
+ throw new DOMException(HIERARCHY_REQUEST_ERR, 'Only one element can be added and only after doctype');
783
+ }
784
+ }
785
+ if (isDocTypeNode(node)) {
786
+ if (parentChildNodes.find(isDocTypeNode)) {
787
+ throw new DOMException(HIERARCHY_REQUEST_ERR, 'Only one doctype is allowed');
788
+ }
789
+ let parentElementChild = parentChildNodes.find(isElementNode);
790
+ if (child && parentChildNodes.indexOf(parentElementChild) < parentChildNodes.indexOf(child)) {
791
+ throw new DOMException(HIERARCHY_REQUEST_ERR, 'Doctype can only be inserted before an element');
792
+ }
793
+ if (!child && parentElementChild) {
794
+ throw new DOMException(HIERARCHY_REQUEST_ERR, 'Doctype can not be appended since element is present');
795
+ }
796
+ }
797
+ }
798
+
799
+ var cp = node.parentNode;
647
800
  if(cp){
648
- cp.removeChild(newChild);//remove and update
801
+ cp.removeChild(node);//remove and update
649
802
  }
650
- if(newChild.nodeType === DOCUMENT_FRAGMENT_NODE){
651
- var newFirst = newChild.firstChild;
803
+ if(node.nodeType === DOCUMENT_FRAGMENT_NODE){
804
+ var newFirst = node.firstChild;
652
805
  if (newFirst == null) {
653
- return newChild;
806
+ return node;
654
807
  }
655
- var newLast = newChild.lastChild;
808
+ var newLast = node.lastChild;
656
809
  }else{
657
- newFirst = newLast = newChild;
810
+ newFirst = newLast = node;
658
811
  }
659
- var pre = nextChild ? nextChild.previousSibling : parentNode.lastChild;
812
+ var pre = child ? child.previousSibling : parent.lastChild;
660
813
 
661
814
  newFirst.previousSibling = pre;
662
- newLast.nextSibling = nextChild;
663
-
664
-
815
+ newLast.nextSibling = child;
816
+
817
+
665
818
  if(pre){
666
819
  pre.nextSibling = newFirst;
667
820
  }else{
668
- parentNode.firstChild = newFirst;
821
+ parent.firstChild = newFirst;
669
822
  }
670
- if(nextChild == null){
671
- parentNode.lastChild = newLast;
823
+ if(child == null){
824
+ parent.lastChild = newLast;
672
825
  }else{
673
- nextChild.previousSibling = newLast;
826
+ child.previousSibling = newLast;
674
827
  }
675
828
  do{
676
- newFirst.parentNode = parentNode;
829
+ newFirst.parentNode = parent;
677
830
  }while(newFirst !== newLast && (newFirst= newFirst.nextSibling))
678
- _onUpdateChild(parentNode.ownerDocument||parentNode,parentNode);
679
- //console.log(parentNode.lastChild.nextSibling == null)
680
- if (newChild.nodeType == DOCUMENT_FRAGMENT_NODE) {
681
- newChild.firstChild = newChild.lastChild = null;
831
+ _onUpdateChild(parent.ownerDocument||parent, parent);
832
+ //console.log(parent.lastChild.nextSibling == null)
833
+ if (node.nodeType == DOCUMENT_FRAGMENT_NODE) {
834
+ node.firstChild = node.lastChild = null;
682
835
  }
683
- return newChild;
836
+ return node;
684
837
  }
685
838
  function _appendSingleChild(parentNode,newChild){
686
839
  var cp = newChild.parentNode;
@@ -703,6 +856,7 @@ function _appendSingleChild(parentNode,newChild){
703
856
  return newChild;
704
857
  //console.log("__aa",parentNode.lastChild.nextSibling == null)
705
858
  }
859
+
706
860
  Document.prototype = {
707
861
  //implementation : null,
708
862
  nodeName : '#document',
@@ -727,11 +881,13 @@ Document.prototype = {
727
881
  }
728
882
  return newChild;
729
883
  }
730
- if(this.documentElement == null && newChild.nodeType == ELEMENT_NODE){
884
+ _insertBefore(this, newChild, refChild);
885
+ newChild.ownerDocument = this;
886
+ if (this.documentElement === null && newChild.nodeType === ELEMENT_NODE) {
731
887
  this.documentElement = newChild;
732
888
  }
733
889
 
734
- return _insertBefore(this,newChild,refChild),(newChild.ownerDocument = this),newChild;
890
+ return newChild;
735
891
  },
736
892
  removeChild : function(oldChild){
737
893
  if(this.documentElement == oldChild){
@@ -925,7 +1081,7 @@ Element.prototype = {
925
1081
  var attr = this.getAttributeNode(name)
926
1082
  attr && this.removeAttributeNode(attr);
927
1083
  },
928
-
1084
+
929
1085
  //four real opeartion method
930
1086
  appendChild:function(newChild){
931
1087
  if(newChild.nodeType === DOCUMENT_FRAGMENT_NODE){
@@ -949,7 +1105,7 @@ Element.prototype = {
949
1105
  var old = this.getAttributeNodeNS(namespaceURI, localName);
950
1106
  old && this.removeAttributeNode(old);
951
1107
  },
952
-
1108
+
953
1109
  hasAttributeNS : function(namespaceURI, localName){
954
1110
  return this.getAttributeNodeNS(namespaceURI, localName)!=null;
955
1111
  },
@@ -965,7 +1121,7 @@ Element.prototype = {
965
1121
  getAttributeNodeNS : function(namespaceURI, localName){
966
1122
  return this.attributes.getNamedItemNS(namespaceURI, localName);
967
1123
  },
968
-
1124
+
969
1125
  getElementsByTagName : function(tagName){
970
1126
  return new LiveNodeList(this,function(base){
971
1127
  var ls = [];
@@ -986,7 +1142,7 @@ Element.prototype = {
986
1142
  }
987
1143
  });
988
1144
  return ls;
989
-
1145
+
990
1146
  });
991
1147
  }
992
1148
  };
@@ -1015,7 +1171,7 @@ CharacterData.prototype = {
1015
1171
  },
1016
1172
  insertData: function(offset,text) {
1017
1173
  this.replaceData(offset,0,text);
1018
-
1174
+
1019
1175
  },
1020
1176
  appendChild:function(newChild){
1021
1177
  throw new Error(ExceptionMessage[HIERARCHY_REQUEST_ERR])
@@ -1109,7 +1265,7 @@ function nodeSerializeToString(isHtml,nodeFilter){
1109
1265
  var refNode = this.nodeType == 9 && this.documentElement || this;
1110
1266
  var prefix = refNode.prefix;
1111
1267
  var uri = refNode.namespaceURI;
1112
-
1268
+
1113
1269
  if(uri && prefix == null){
1114
1270
  //console.log(prefix)
1115
1271
  var prefix = refNode.lookupPrefix(uri);
@@ -1142,8 +1298,8 @@ function needNamespaceDefine(node, isHTML, visibleNamespaces) {
1142
1298
  if (prefix === "xml" && uri === NAMESPACE.XML || uri === NAMESPACE.XMLNS) {
1143
1299
  return false;
1144
1300
  }
1145
-
1146
- var i = visibleNamespaces.length
1301
+
1302
+ var i = visibleNamespaces.length
1147
1303
  while (i--) {
1148
1304
  var ns = visibleNamespaces[i];
1149
1305
  // get namespace prefix
@@ -1187,7 +1343,7 @@ function serializeToString(node,buf,isHTML,nodeFilter,visibleNamespaces){
1187
1343
  var len = attrs.length;
1188
1344
  var child = node.firstChild;
1189
1345
  var nodeName = node.tagName;
1190
-
1346
+
1191
1347
  isHTML = NAMESPACE.isHTML(node.namespaceURI) || isHTML
1192
1348
 
1193
1349
  var prefixedNodeName = nodeName
@@ -1246,14 +1402,14 @@ function serializeToString(node,buf,isHTML,nodeFilter,visibleNamespaces){
1246
1402
  serializeToString(attr,buf,isHTML,nodeFilter,visibleNamespaces);
1247
1403
  }
1248
1404
 
1249
- // add namespace for current node
1405
+ // add namespace for current node
1250
1406
  if (nodeName === prefixedNodeName && needNamespaceDefine(node, isHTML, visibleNamespaces)) {
1251
1407
  var prefix = node.prefix||'';
1252
1408
  var uri = node.namespaceURI;
1253
1409
  addSerializedAttribute(buf, prefix ? 'xmlns:' + prefix : "xmlns", uri);
1254
1410
  visibleNamespaces.push({ prefix: prefix, namespace:uri });
1255
1411
  }
1256
-
1412
+
1257
1413
  if(child || isHTML && !/^(?:meta|link|img|br|hr|input)$/i.test(nodeName)){
1258
1414
  buf.push('>');
1259
1415
  //if is cdata child node
@@ -1468,7 +1624,7 @@ try{
1468
1624
  }
1469
1625
  }
1470
1626
  })
1471
-
1627
+
1472
1628
  function getTextContent(node){
1473
1629
  switch(node.nodeType){
1474
1630
  case ELEMENT_NODE:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmldom/xmldom",
3
- "version": "0.7.6",
3
+ "version": "0.7.7",
4
4
  "description": "A pure JavaScript W3C standard-based (XML DOM Level 2 Core) DOMParser and XMLSerializer module.",
5
5
  "keywords": [
6
6
  "w3c",