@xmldom/xmldom 0.9.0-beta.4 → 0.9.0-beta.6

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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,42 @@ 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.9.0-beta.6](https://github.com/xmldom/xmldom/compare/0.9.0-beta.5...0.9.0-beta.)
8
+
9
+ ### Fixed
10
+
11
+ - Properly check nodes before replacement [`#457`](https://github.com/xmldom/xmldom/pull/457) / [`#455`](https://github.com/xmldom/xmldom/issues/455)
12
+
13
+ Thank you, [@edemaine](https://github.com/edemaine), [@pedro-l9](https://github.com/pedro-l9), for your contributions
14
+
15
+
16
+ ## [0.9.0-beta.5](https://github.com/xmldom/xmldom/compare/0.9.0-beta.4...0.9.0-beta.5)
17
+
18
+ ### Fixed
19
+
20
+ - fix: Restore ES5 compatibility [`#452`](https://github.com/xmldom/xmldom/pull/452) / [`#453`](https://github.com/xmldom/xmldom/issues/453)
21
+
22
+ Thank you, [@fengxinming](https://github.com/fengxinming), for your contributions
23
+
24
+
25
+ ## [0.8.5](https://github.com/xmldom/xmldom/compare/0.8.4...0.8.5)
26
+
27
+ ### Fixed
28
+
29
+ - fix: Restore ES5 compatibility [`#452`](https://github.com/xmldom/xmldom/pull/452) / [`#453`](https://github.com/xmldom/xmldom/issues/453)
30
+
31
+ Thank you, [@fengxinming](https://github.com/fengxinming), for your contributions
32
+
33
+
34
+ ## [0.7.8](https://github.com/xmldom/xmldom/compare/0.7.7...0.7.8)
35
+
36
+ ### Fixed
37
+
38
+ - fix: Restore ES5 compatibility [`#452`](https://github.com/xmldom/xmldom/pull/452) / [`#453`](https://github.com/xmldom/xmldom/issues/453)
39
+
40
+ Thank you, [@fengxinming](https://github.com/fengxinming), for your contributions
41
+
42
+
7
43
  ## [0.9.0-beta.4](https://github.com/xmldom/xmldom/compare/0.9.0-beta.3...0.9.0-beta.4)
8
44
 
9
45
  ### Fixed
@@ -22,6 +58,33 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
22
58
 
23
59
  Thank you, [@XhmikosR](https://github.com/XhmikosR), [@awwright](https://github.com/awwright), [@frumioj](https://github.com/frumioj), [@cjbarth](https://github.com/cjbarth), [@markgollnick](https://github.com/markgollnick) for your contributions
24
60
 
61
+
62
+ ## [0.8.4](https://github.com/xmldom/xmldom/compare/0.8.3...0.8.4)
63
+
64
+ ### Fixed
65
+
66
+ - 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)
67
+ 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.
68
+ In the upcoming version 0.9.0 those text nodes will no longer be added and an error will be thrown instead.
69
+ 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.
70
+ Related Spec: <https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity>
71
+
72
+ Thank you, [@frumioj](https://github.com/frumioj), [@cjbarth](https://github.com/cjbarth), [@markgollnick](https://github.com/markgollnick) for your contributions
73
+
74
+
75
+ ## [0.7.7](https://github.com/xmldom/xmldom/compare/0.7.6...0.7.7)
76
+
77
+ ### Fixed
78
+
79
+ - 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)
80
+ 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.
81
+ In the upcoming version 0.9.0 those text nodes will no longer be added and an error will be thrown instead.
82
+ 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.
83
+ Related Spec: <https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity>
84
+
85
+ Thank you, [@frumioj](https://github.com/frumioj), [@cjbarth](https://github.com/cjbarth), [@markgollnick](https://github.com/markgollnick) for your contributions
86
+
87
+
25
88
  ## [0.9.0-beta.3](https://github.com/xmldom/xmldom/compare/0.9.0-beta.2...0.9.0-beta.3)
26
89
 
27
90
  ### Fixed
@@ -1,5 +1,37 @@
1
1
  'use strict';
2
2
 
3
+ /**
4
+ * Ponyfill for `Array.prototype.find` which is only available in ES6 runtimes.
5
+ *
6
+ * Works with anything that has a `length` property and index access properties, including NodeList.
7
+ *
8
+ * @template {unknown} T
9
+ * @param {Array<T> | ({length:number, [number]: T})} list
10
+ * @param {function (item: T, index: number, list:Array<T> | ({length:number, [number]: T})):boolean} predicate
11
+ * @param {Partial<Pick<ArrayConstructor['prototype'], 'find'>>?} ac `Array.prototype` by default,
12
+ * allows injecting a custom implementation in tests
13
+ * @returns {T | undefined}
14
+ *
15
+ * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find
16
+ * @see https://tc39.es/ecma262/multipage/indexed-collections.html#sec-array.prototype.find
17
+ */
18
+ function find(list, predicate, ac) {
19
+ if (ac === undefined) {
20
+ ac = Array.prototype;
21
+ }
22
+ if (list && typeof ac.find === 'function') {
23
+ return ac.find.call(list, predicate);
24
+ }
25
+ for (var i = 0; i < list.length; i++) {
26
+ if (Object.prototype.hasOwnProperty.call(list, i)) {
27
+ var item = list[i];
28
+ if (predicate.call(undefined, item, i, list)) {
29
+ return item;
30
+ }
31
+ }
32
+ }
33
+ }
34
+
3
35
  /**
4
36
  * "Shallow freezes" an object to render it immutable.
5
37
  * Uses `Object.freeze` if available,
@@ -330,6 +362,7 @@ var NAMESPACE = freeze({
330
362
  });
331
363
 
332
364
  exports.assign = assign;
365
+ exports.find = find;
333
366
  exports.freeze = freeze;
334
367
  exports.HTML_BOOLEAN_ATTRIBUTES = HTML_BOOLEAN_ATTRIBUTES;
335
368
  exports.HTML_RAW_TEXT_ELEMENTS = HTML_RAW_TEXT_ELEMENTS;
package/lib/dom.js CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var conventions = require('./conventions');
4
+ var find = conventions.find;
4
5
  var isHTMLRawTextElement = conventions.isHTMLRawTextElement;
5
6
  var isHTMLVoidElement = conventions.isHTMLVoidElement;
6
7
  var MIME_TYPE = conventions.MIME_TYPE;
@@ -180,14 +181,6 @@ NodeList.prototype = {
180
181
  }
181
182
  return buf.join('');
182
183
  },
183
- /**
184
- * @private
185
- * @param {function (Node):boolean} predicate
186
- * @returns {Node | undefined}
187
- */
188
- find: function (predicate) {
189
- return Array.prototype.find.call(this, predicate);
190
- },
191
184
  /**
192
185
  * @private
193
186
  * @param {function (Node):boolean} predicate
@@ -521,7 +514,7 @@ Node.prototype = {
521
514
  },
522
515
  replaceChild: function (newChild, oldChild) {
523
516
  //raises
524
- this.insertBefore(newChild, oldChild);
517
+ _insertBefore(this, newChild, oldChild, assertPreReplacementValidityInDocument);
525
518
  if (oldChild) {
526
519
  this.removeChild(oldChild);
527
520
  }
@@ -832,14 +825,41 @@ function isTextNode(node) {
832
825
  */
833
826
  function isElementInsertionPossible(doc, child) {
834
827
  var parentChildNodes = doc.childNodes || [];
835
- if (parentChildNodes.find(isElementNode) || isDocTypeNode(child)) {
828
+ if (find(parentChildNodes, isElementNode) || isDocTypeNode(child)) {
836
829
  return false;
837
830
  }
838
- var docTypeNode = parentChildNodes.find(isDocTypeNode);
831
+ var docTypeNode = find(parentChildNodes, isDocTypeNode);
839
832
  return !(child && docTypeNode && parentChildNodes.indexOf(docTypeNode) > parentChildNodes.indexOf(child));
840
833
  }
834
+
841
835
  /**
836
+ * Check if en element node can be inserted before `child`, or at the end if child is falsy,
837
+ * according to the presence and position of a doctype node on the same level.
838
+ *
839
+ * @param {Node} doc The document node
840
+ * @param {Node} child the node that would become the nextSibling if the element would be inserted
841
+ * @returns {boolean} `true` if an element can be inserted before child
842
842
  * @private
843
+ * https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity
844
+ */
845
+ function isElementReplacementPossible(doc, child) {
846
+ var parentChildNodes = doc.childNodes || [];
847
+
848
+ function hasElementChildThatIsNotChild(node) {
849
+ return isElementNode(node) && node !== child;
850
+ }
851
+
852
+ if (find(parentChildNodes, hasElementChildThatIsNotChild)) {
853
+ return false;
854
+ }
855
+ var docTypeNode = find(parentChildNodes, isDocTypeNode);
856
+ return !(child && docTypeNode && parentChildNodes.indexOf(docTypeNode) > parentChildNodes.indexOf(child));
857
+ }
858
+
859
+ /**
860
+ * @private
861
+ * Steps 1-5 of the checks before inserting and before replacing a child are the same.
862
+ *
843
863
  * @param {Node} parent the parent node to insert `node` into
844
864
  * @param {Node} node the node to insert
845
865
  * @param {Node=} child the node that should become the `nextSibling` of `node`
@@ -847,18 +867,26 @@ function isElementInsertionPossible(doc, child) {
847
867
  * @throws DOMException for several node combinations that would create a DOM that is not well-formed.
848
868
  * @throws DOMException if `child` is provided but is not a child of `parent`.
849
869
  * @see https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity
870
+ * @see https://dom.spec.whatwg.org/#concept-node-replace
850
871
  */
851
- function _insertBefore(parent, node, child) {
872
+ function assertPreInsertionValidity1to5(parent, node, child) {
873
+ // 1. If `parent` is not a Document, DocumentFragment, or Element node, then throw a "HierarchyRequestError" DOMException.
852
874
  if (!hasValidParentNodeType(parent)) {
853
875
  throw new DOMException(HIERARCHY_REQUEST_ERR, 'Unexpected parent node type ' + parent.nodeType);
854
876
  }
877
+ // 2. If `node` is a host-including inclusive ancestor of `parent`, then throw a "HierarchyRequestError" DOMException.
878
+ // not implemented!
879
+ // 3. If `child` is non-null and its parent is not `parent`, then throw a "NotFoundError" DOMException.
855
880
  if (child && child.parentNode !== parent) {
856
881
  throw new DOMException(NOT_FOUND_ERR, 'child not in parent');
857
882
  }
858
883
  if (
884
+ // 4. If `node` is not a DocumentFragment, DocumentType, Element, or CharacterData node, then throw a "HierarchyRequestError" DOMException.
859
885
  !hasInsertableNodeType(node) ||
886
+ // 5. If either `node` is a Text node and `parent` is a document,
860
887
  // the sax parser currently adds top level text nodes, this will be fixed in 0.9.0
861
888
  // || (node.nodeType === Node.TEXT_NODE && parent.nodeType === Node.DOCUMENT_NODE)
889
+ // or `node` is a doctype and `parent` is not a document, then throw a "HierarchyRequestError" DOMException.
862
890
  (isDocTypeNode(node) && parent.nodeType !== Node.DOCUMENT_NODE)
863
891
  ) {
864
892
  throw new DOMException(
@@ -866,36 +894,137 @@ function _insertBefore(parent, node, child) {
866
894
  'Unexpected node type ' + node.nodeType + ' for parent node type ' + parent.nodeType
867
895
  );
868
896
  }
897
+ }
898
+
899
+ /**
900
+ * @private
901
+ * Step 6 of the checks before inserting and before replacing a child are different.
902
+ *
903
+ * @param {Document} parent the parent node to insert `node` into
904
+ * @param {Node} node the node to insert
905
+ * @param {Node | undefined} child the node that should become the `nextSibling` of `node`
906
+ * @returns {Node}
907
+ * @throws DOMException for several node combinations that would create a DOM that is not well-formed.
908
+ * @throws DOMException if `child` is provided but is not a child of `parent`.
909
+ * @see https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity
910
+ * @see https://dom.spec.whatwg.org/#concept-node-replace
911
+ */
912
+ function assertPreInsertionValidityInDocument(parent, node, child) {
869
913
  var parentChildNodes = parent.childNodes || [];
870
914
  var nodeChildNodes = node.childNodes || [];
871
- if (parent.nodeType === Node.DOCUMENT_NODE) {
872
- if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
873
- let nodeChildElements = nodeChildNodes.filter(isElementNode);
874
- if (nodeChildElements.length > 1 || nodeChildNodes.find(isTextNode)) {
875
- throw new DOMException(HIERARCHY_REQUEST_ERR, 'More than one element or text in fragment');
876
- }
877
- if (nodeChildElements.length === 1 && !isElementInsertionPossible(parent, child)) {
878
- throw new DOMException(HIERARCHY_REQUEST_ERR, 'Element in fragment can not be inserted before doctype');
879
- }
915
+
916
+ // DocumentFragment
917
+ if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
918
+ var nodeChildElements = nodeChildNodes.filter(isElementNode);
919
+ // If node has more than one element child or has a Text node child.
920
+ if (nodeChildElements.length > 1 || find(nodeChildNodes, isTextNode)) {
921
+ throw new DOMException(HIERARCHY_REQUEST_ERR, 'More than one element or text in fragment');
880
922
  }
881
- if (isElementNode(node)) {
882
- if (parentChildNodes.find(isElementNode) || !isElementInsertionPossible(parent, child)) {
883
- throw new DOMException(HIERARCHY_REQUEST_ERR, 'Only one element can be added and only after doctype');
884
- }
923
+ // Otherwise, if `node` has one element child and either `parent` has an element child,
924
+ // `child` is a doctype, or `child` is non-null and a doctype is following `child`.
925
+ if (nodeChildElements.length === 1 && !isElementInsertionPossible(parent, child)) {
926
+ throw new DOMException(HIERARCHY_REQUEST_ERR, 'Element in fragment can not be inserted before doctype');
885
927
  }
886
- if (isDocTypeNode(node)) {
887
- if (parentChildNodes.find(isDocTypeNode)) {
888
- throw new DOMException(HIERARCHY_REQUEST_ERR, 'Only one doctype is allowed');
889
- }
890
- let parentElementChild = parentChildNodes.find(isElementNode);
891
- if (child && parentChildNodes.indexOf(parentElementChild) < parentChildNodes.indexOf(child)) {
892
- throw new DOMException(HIERARCHY_REQUEST_ERR, 'Doctype can only be inserted before an element');
893
- }
894
- if (!child && parentElementChild) {
895
- throw new DOMException(HIERARCHY_REQUEST_ERR, 'Doctype can not be appended since element is present');
896
- }
928
+ }
929
+ // Element
930
+ if (isElementNode(node)) {
931
+ // `parent` has an element child, `child` is a doctype,
932
+ // or `child` is non-null and a doctype is following `child`.
933
+ if (!isElementInsertionPossible(parent, child)) {
934
+ throw new DOMException(HIERARCHY_REQUEST_ERR, 'Only one element can be added and only after doctype');
935
+ }
936
+ }
937
+ // DocumentType
938
+ if (isDocTypeNode(node)) {
939
+ // `parent` has a doctype child,
940
+ if (find(parentChildNodes, isDocTypeNode)) {
941
+ throw new DOMException(HIERARCHY_REQUEST_ERR, 'Only one doctype is allowed');
942
+ }
943
+ var parentElementChild = find(parentChildNodes, isElementNode);
944
+ // `child` is non-null and an element is preceding `child`,
945
+ if (child && parentChildNodes.indexOf(parentElementChild) < parentChildNodes.indexOf(child)) {
946
+ throw new DOMException(HIERARCHY_REQUEST_ERR, 'Doctype can only be inserted before an element');
947
+ }
948
+ // or `child` is null and `parent` has an element child.
949
+ if (!child && parentElementChild) {
950
+ throw new DOMException(HIERARCHY_REQUEST_ERR, 'Doctype can not be appended since element is present');
897
951
  }
898
952
  }
953
+ }
954
+
955
+ /**
956
+ * @private
957
+ * Step 6 of the checks before inserting and before replacing a child are different.
958
+ *
959
+ * @param {Document} parent the parent node to insert `node` into
960
+ * @param {Node} node the node to insert
961
+ * @param {Node | undefined} child the node that should become the `nextSibling` of `node`
962
+ * @returns {Node}
963
+ * @throws DOMException for several node combinations that would create a DOM that is not well-formed.
964
+ * @throws DOMException if `child` is provided but is not a child of `parent`.
965
+ * @see https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity
966
+ * @see https://dom.spec.whatwg.org/#concept-node-replace
967
+ */
968
+ function assertPreReplacementValidityInDocument(parent, node, child) {
969
+ var parentChildNodes = parent.childNodes || [];
970
+ var nodeChildNodes = node.childNodes || [];
971
+
972
+ // DocumentFragment
973
+ if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
974
+ var nodeChildElements = nodeChildNodes.filter(isElementNode);
975
+ // If `node` has more than one element child or has a Text node child.
976
+ if (nodeChildElements.length > 1 || find(nodeChildNodes, isTextNode)) {
977
+ throw new DOMException(HIERARCHY_REQUEST_ERR, 'More than one element or text in fragment');
978
+ }
979
+ // Otherwise, if `node` has one element child and either `parent` has an element child that is not `child` or a doctype is following `child`.
980
+ if (nodeChildElements.length === 1 && !isElementReplacementPossible(parent, child)) {
981
+ throw new DOMException(HIERARCHY_REQUEST_ERR, 'Element in fragment can not be inserted before doctype');
982
+ }
983
+ }
984
+ // Element
985
+ if (isElementNode(node)) {
986
+ // `parent` has an element child that is not `child` or a doctype is following `child`.
987
+ if (!isElementReplacementPossible(parent, child)) {
988
+ throw new DOMException(HIERARCHY_REQUEST_ERR, 'Only one element can be added and only after doctype');
989
+ }
990
+ }
991
+ // DocumentType
992
+ if (isDocTypeNode(node)) {
993
+ function hasDoctypeChildThatIsNotChild(node) {
994
+ return isDocTypeNode(node) && node !== child;
995
+ }
996
+
997
+ // `parent` has a doctype child that is not `child`,
998
+ if (find(parentChildNodes, hasDoctypeChildThatIsNotChild)) {
999
+ throw new DOMException(HIERARCHY_REQUEST_ERR, 'Only one doctype is allowed');
1000
+ }
1001
+ var parentElementChild = find(parentChildNodes, isElementNode);
1002
+ // or an element is preceding `child`.
1003
+ if (child && parentChildNodes.indexOf(parentElementChild) < parentChildNodes.indexOf(child)) {
1004
+ throw new DOMException(HIERARCHY_REQUEST_ERR, 'Doctype can only be inserted before an element');
1005
+ }
1006
+ }
1007
+ }
1008
+
1009
+ /**
1010
+ * @private
1011
+ * @param {Node} parent the parent node to insert `node` into
1012
+ * @param {Node} node the node to insert
1013
+ * @param {Node=} child the node that should become the `nextSibling` of `node`
1014
+ * @returns {Node}
1015
+ * @throws DOMException for several node combinations that would create a DOM that is not well-formed.
1016
+ * @throws DOMException if `child` is provided but is not a child of `parent`.
1017
+ * @see https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity
1018
+ */
1019
+ function _insertBefore(parent, node, child, _inDocumentAssertion) {
1020
+ // To ensure pre-insertion validity of a node into a parent before a child, run these steps:
1021
+ assertPreInsertionValidity1to5(parent, node, child);
1022
+
1023
+ // If parent is a document, and any of the statements below, switched on the interface node implements,
1024
+ // are true, then throw a "HierarchyRequestError" DOMException.
1025
+ if (parent.nodeType === Node.DOCUMENT_NODE) {
1026
+ (_inDocumentAssertion || assertPreInsertionValidityInDocument)(parent, node, child);
1027
+ }
899
1028
 
900
1029
  var cp = node.parentNode;
901
1030
  if (cp) {
@@ -1008,6 +1137,17 @@ Document.prototype = {
1008
1137
  }
1009
1138
  return _removeChild(this, oldChild);
1010
1139
  },
1140
+ replaceChild: function (newChild, oldChild) {
1141
+ //raises
1142
+ _insertBefore(this, newChild, oldChild, assertPreReplacementValidityInDocument);
1143
+ newChild.ownerDocument = this;
1144
+ if (oldChild) {
1145
+ this.removeChild(oldChild);
1146
+ }
1147
+ if (isElementNode(newChild)) {
1148
+ this.documentElement = newChild;
1149
+ }
1150
+ },
1011
1151
  // Introduced in DOM Level 2:
1012
1152
  importNode: function (importedNode, deep) {
1013
1153
  return importNode(this, importedNode, deep);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmldom/xmldom",
3
- "version": "0.9.0-beta.4",
3
+ "version": "0.9.0-beta.6",
4
4
  "description": "A pure JavaScript W3C standard-based (XML DOM Level 2 Core) DOMParser and XMLSerializer module.",
5
5
  "keywords": [
6
6
  "w3c",
@@ -35,8 +35,9 @@
35
35
  "stryker": "stryker run",
36
36
  "stryker:dry-run": "stryker run -m '' --reporters progress",
37
37
  "test": "jest",
38
+ "testrelease": "npm test && eslint lib",
38
39
  "version": "./changelog-has-version.sh",
39
- "release": "np --no-yarn"
40
+ "release": "np --no-yarn --test-script testrelease"
40
41
  },
41
42
  "engines": {
42
43
  "node": ">=10.0.0"