dompurify 3.4.4 → 3.4.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.
@@ -1,4 +1,4 @@
1
- /*! @license DOMPurify 3.4.4 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.4/LICENSE */
1
+ /*! @license DOMPurify 3.4.6 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.6/LICENSE */
2
2
 
3
3
  'use strict';
4
4
 
@@ -298,7 +298,7 @@ function isRegex(value) {
298
298
  }
299
299
  }
300
300
 
301
- const html$1 = freeze(['a', 'abbr', 'acronym', 'address', 'area', 'article', 'aside', 'audio', 'b', 'bdi', 'bdo', 'big', 'blink', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'content', 'data', 'datalist', 'dd', 'decorator', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'element', 'em', 'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'main', 'map', 'mark', 'marquee', 'menu', 'menuitem', 'meter', 'nav', 'nobr', 'ol', 'optgroup', 'option', 'output', 'p', 'picture', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'search', 'section', 'select', 'selectedcontent', 'shadow', 'slot', 'small', 'source', 'spacer', 'span', 'strike', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'tr', 'track', 'tt', 'u', 'ul', 'var', 'video', 'wbr']);
301
+ const html$1 = freeze(['a', 'abbr', 'acronym', 'address', 'area', 'article', 'aside', 'audio', 'b', 'bdi', 'bdo', 'big', 'blink', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'content', 'data', 'datalist', 'dd', 'decorator', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'element', 'em', 'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'main', 'map', 'mark', 'marquee', 'menu', 'menuitem', 'meter', 'nav', 'nobr', 'ol', 'optgroup', 'option', 'output', 'p', 'picture', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'search', 'section', 'select', 'shadow', 'slot', 'small', 'source', 'spacer', 'span', 'strike', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'tr', 'track', 'tt', 'u', 'ul', 'var', 'video', 'wbr']);
302
302
  const svg$1 = freeze(['svg', 'a', 'altglyph', 'altglyphdef', 'altglyphitem', 'animatecolor', 'animatemotion', 'animatetransform', 'circle', 'clippath', 'defs', 'desc', 'ellipse', 'enterkeyhint', 'exportparts', 'filter', 'font', 'g', 'glyph', 'glyphref', 'hkern', 'image', 'inputmode', 'line', 'lineargradient', 'marker', 'mask', 'metadata', 'mpath', 'part', 'path', 'pattern', 'polygon', 'polyline', 'radialgradient', 'rect', 'stop', 'style', 'switch', 'symbol', 'text', 'textpath', 'title', 'tref', 'tspan', 'view', 'vkern']);
303
303
  const svgFilters = freeze(['feBlend', 'feColorMatrix', 'feComponentTransfer', 'feComposite', 'feConvolveMatrix', 'feDiffuseLighting', 'feDisplacementMap', 'feDistantLight', 'feDropShadow', 'feFlood', 'feFuncA', 'feFuncB', 'feFuncG', 'feFuncR', 'feGaussianBlur', 'feImage', 'feMerge', 'feMergeNode', 'feMorphology', 'feOffset', 'fePointLight', 'feSpecularLighting', 'feSpotLight', 'feTile', 'feTurbulence']);
304
304
  // List of SVG elements that are disallowed by default.
@@ -334,11 +334,20 @@ const CUSTOM_ELEMENT = seal(/^[a-z][.\w]*(-[.\w]+)+$/i);
334
334
  // https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
335
335
  const NODE_TYPE = {
336
336
  element: 1,
337
+ attribute: 2,
337
338
  text: 3,
339
+ cdataSection: 4,
340
+ entityReference: 5,
341
+ // Deprecated
342
+ entityNode: 6,
338
343
  // Deprecated
339
344
  progressingInstruction: 7,
340
345
  comment: 8,
341
- document: 9};
346
+ document: 9,
347
+ documentType: 10,
348
+ documentFragment: 11,
349
+ notation: 12 // Deprecated
350
+ };
342
351
  const getGlobal = function getGlobal() {
343
352
  return typeof window === 'undefined' ? null : window;
344
353
  };
@@ -396,7 +405,7 @@ const _createHooksMap = function _createHooksMap() {
396
405
  function createDOMPurify() {
397
406
  let window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal();
398
407
  const DOMPurify = root => createDOMPurify(root);
399
- DOMPurify.version = '3.4.4';
408
+ DOMPurify.version = '3.4.6';
400
409
  DOMPurify.removed = [];
401
410
  if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) {
402
411
  // Not running in a browser, provide a factory function
@@ -407,15 +416,15 @@ function createDOMPurify() {
407
416
  let document = window.document;
408
417
  const originalDocument = document;
409
418
  const currentScript = originalDocument.currentScript;
410
- const DocumentFragment = window.DocumentFragment,
411
- HTMLTemplateElement = window.HTMLTemplateElement,
419
+ window.DocumentFragment;
420
+ const HTMLTemplateElement = window.HTMLTemplateElement,
412
421
  Node = window.Node,
413
422
  Element = window.Element,
414
423
  NodeFilter = window.NodeFilter,
415
- _window$NamedNodeMap = window.NamedNodeMap,
416
- NamedNodeMap = _window$NamedNodeMap === void 0 ? window.NamedNodeMap || window.MozNamedAttrMap : _window$NamedNodeMap,
417
- HTMLFormElement = window.HTMLFormElement,
418
- DOMParser = window.DOMParser,
424
+ _window$NamedNodeMap = window.NamedNodeMap;
425
+ _window$NamedNodeMap === void 0 ? window.NamedNodeMap || window.MozNamedAttrMap : _window$NamedNodeMap;
426
+ window.HTMLFormElement;
427
+ const DOMParser = window.DOMParser,
419
428
  trustedTypes = window.trustedTypes;
420
429
  const ElementPrototype = Element.prototype;
421
430
  const cloneNode = lookupGetter(ElementPrototype, 'cloneNode');
@@ -423,7 +432,10 @@ function createDOMPurify() {
423
432
  const getNextSibling = lookupGetter(ElementPrototype, 'nextSibling');
424
433
  const getChildNodes = lookupGetter(ElementPrototype, 'childNodes');
425
434
  const getParentNode = lookupGetter(ElementPrototype, 'parentNode');
435
+ const getShadowRoot = lookupGetter(ElementPrototype, 'shadowRoot');
436
+ const getAttributes = lookupGetter(ElementPrototype, 'attributes');
426
437
  const getNodeType = Node && Node.prototype ? lookupGetter(Node.prototype, 'nodeType') : null;
438
+ const getNodeName = Node && Node.prototype ? lookupGetter(Node.prototype, 'nodeName') : null;
427
439
  // As per issue #47, the web-components registry is inherited by a
428
440
  // new document created via createHTMLDocument. As per the spec
429
441
  // (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries)
@@ -1014,11 +1026,100 @@ function createDOMPurify() {
1014
1026
  /**
1015
1027
  * _isClobbered
1016
1028
  *
1029
+ * Detect DOM-clobbering on HTMLFormElement nodes. Form is the only HTML
1030
+ * interface with [LegacyOverrideBuiltIns]; a descendant element with a
1031
+ * `name` attribute matching a prototype property shadows that property
1032
+ * on direct reads. We use this check at the IN_PLACE entry-point and
1033
+ * during attribute sanitization to refuse clobbered forms.
1034
+ *
1035
+ * Realm safety (GHSA-hpcv-96wg-7vj8): every check in this function must
1036
+ * work for foreign-realm forms — e.g. a <form> created inside a same-
1037
+ * origin iframe and then handed to a parent-realm DOMPurify instance
1038
+ * with IN_PLACE: true. The original implementation used
1039
+ * `element instanceof HTMLFormElement` and `element.attributes
1040
+ * instanceof NamedNodeMap`, both of which are realm-bound: a foreign-
1041
+ * realm form is an instance of the *foreign* realm's HTMLFormElement,
1042
+ * not the parent realm's. The instanceof short-circuited to false and
1043
+ * the function returned false (= not clobbered) regardless of how
1044
+ * thoroughly the form was clobbered. Sanitize then walked a clobbered
1045
+ * .attributes and missed every attribute on the form root, leaving
1046
+ * onmouseover / onclick / formaction / etc. intact.
1047
+ *
1048
+ * The realm-independent replacements:
1049
+ * - HTMLFormElement detection — read the tag name through the cached
1050
+ * Node.prototype.nodeName getter. WebIDL getters operate on internal
1051
+ * slots that exist on every real Node regardless of which realm
1052
+ * minted the JS wrapper, so getNodeName(foreignForm) === "FORM".
1053
+ * - NamedNodeMap detection — compare the direct .attributes read
1054
+ * against the cached Element.prototype.attributes getter. Same
1055
+ * equality-probe pattern we use for .childNodes: if a clobbering
1056
+ * child shadows the named property, the two reads diverge; if not,
1057
+ * both return the same NamedNodeMap (same-realm OR foreign-realm —
1058
+ * doesn't matter, both are the canonical attributes object for the
1059
+ * node).
1060
+ *
1017
1061
  * @param element element to check for clobbering attacks
1018
1062
  * @return true if clobbered, false if safe
1019
1063
  */
1020
1064
  const _isClobbered = function _isClobbered(element) {
1021
- return element instanceof HTMLFormElement && (typeof element.nodeName !== 'string' || typeof element.textContent !== 'string' || typeof element.removeChild !== 'function' || !(element.attributes instanceof NamedNodeMap) || typeof element.removeAttribute !== 'function' || typeof element.setAttribute !== 'function' || typeof element.namespaceURI !== 'string' || typeof element.insertBefore !== 'function' || typeof element.hasChildNodes !== 'function');
1065
+ // Realm-independent tag-name probe. If we can't determine the tag
1066
+ // name at all, we can't reason about clobbering — return false
1067
+ // (the caller's other defences still apply).
1068
+ const realTagName = getNodeName ? getNodeName(element) : null;
1069
+ if (typeof realTagName !== 'string') {
1070
+ return false;
1071
+ }
1072
+ if (transformCaseFunc(realTagName) !== 'form') {
1073
+ return false;
1074
+ }
1075
+ return typeof element.nodeName !== 'string' || typeof element.textContent !== 'string' || typeof element.removeChild !== 'function' ||
1076
+ // Realm-safe NamedNodeMap detection: equality against the cached
1077
+ // prototype getter. Clobbered .attributes (e.g. <input name="attributes">)
1078
+ // makes the direct read diverge from the cached read; a clean form
1079
+ // (same-realm OR foreign-realm) has both reads pointing at the same
1080
+ // canonical NamedNodeMap.
1081
+ element.attributes !== getAttributes(element) || typeof element.removeAttribute !== 'function' || typeof element.setAttribute !== 'function' || typeof element.namespaceURI !== 'string' || typeof element.insertBefore !== 'function' || typeof element.hasChildNodes !== 'function' ||
1082
+ // HTMLFormElement has [LegacyOverrideBuiltIns]: a descendant named
1083
+ // "childNodes" shadows the prototype getter. Direct reads of
1084
+ // form.childNodes from a clobbered form return the named child
1085
+ // instead of the real NodeList, so any walk that reads it directly
1086
+ // skips the form's real children. Compare the direct read to the
1087
+ // cached Node.prototype getter — when the form's named-property
1088
+ // getter intercepts the read, the two values differ and we flag
1089
+ // the form. This catches every clobbering child type (input,
1090
+ // select, etc.) regardless of whether the named child happens to
1091
+ // carry a numeric .length, which a typeof-based probe would miss
1092
+ // (e.g. HTMLSelectElement.length is a defined unsigned-long).
1093
+ element.childNodes !== getChildNodes(element);
1094
+ };
1095
+ /**
1096
+ * Checks whether the given value is a DocumentFragment from any realm.
1097
+ *
1098
+ * Realm safety (GHSA-hpcv-96wg-7vj8): the original sites used
1099
+ * `value instanceof DocumentFragment`, which is realm-bound — a fragment
1100
+ * from a foreign realm (template content or shadow root from an iframe
1101
+ * document) is an instance of the foreign realm's DocumentFragment, not
1102
+ * the parent realm's, so the check returned false and the template-
1103
+ * content / shadow-root recursion was silently skipped. The attacker
1104
+ * payload inside survived untouched.
1105
+ *
1106
+ * The realm-independent replacement reads `nodeType` through the cached
1107
+ * Node.prototype getter and compares to the DOCUMENT_FRAGMENT_NODE
1108
+ * constant (11). nodeType is a numeric value resolved from the node's
1109
+ * internal slot, identical across realms for the same kind of node.
1110
+ *
1111
+ * @param value object to check
1112
+ * @return true if value is a DocumentFragment-shaped node from any realm
1113
+ */
1114
+ const _isDocumentFragment = function _isDocumentFragment(value) {
1115
+ if (!getNodeType || typeof value !== 'object' || value === null) {
1116
+ return false;
1117
+ }
1118
+ try {
1119
+ return getNodeType(value) === NODE_TYPE.documentFragment;
1120
+ } catch (_) {
1121
+ return false;
1122
+ }
1022
1123
  };
1023
1124
  /**
1024
1125
  * Checks whether the given object is a DOM node, including nodes that
@@ -1123,8 +1224,14 @@ function createDOMPurify() {
1123
1224
  _forceRemove(currentNode);
1124
1225
  return true;
1125
1226
  }
1126
- /* Check whether element has a valid namespace */
1127
- if (currentNode instanceof Element && !_checkValidNamespace(currentNode)) {
1227
+ /* Check whether element has a valid namespace.
1228
+ Realm-safe check (GHSA-hpcv-96wg-7vj8): use the cached Node.prototype
1229
+ nodeType getter rather than `instanceof Element`, which is realm-
1230
+ bound and short-circuits to false for any node minted in a different
1231
+ realm — letting a foreign-realm element with a forbidden namespace
1232
+ slip past the namespace check entirely. */
1233
+ const nt = getNodeType ? getNodeType(currentNode) : currentNode.nodeType;
1234
+ if (nt === NODE_TYPE.element && !_checkValidNamespace(currentNode)) {
1128
1235
  _forceRemove(currentNode);
1129
1236
  return true;
1130
1237
  }
@@ -1351,8 +1458,11 @@ function createDOMPurify() {
1351
1458
  _sanitizeElements(shadowNode);
1352
1459
  /* Check attributes next */
1353
1460
  _sanitizeAttributes(shadowNode);
1354
- /* Deep shadow DOM detected */
1355
- if (shadowNode.content instanceof DocumentFragment) {
1461
+ /* Deep shadow DOM detected.
1462
+ Realm-safe check (GHSA-hpcv-96wg-7vj8): use nodeType against the
1463
+ DOCUMENT_FRAGMENT_NODE constant rather than instanceof, so we
1464
+ recurse into <template>.content from foreign realms too. */
1465
+ if (_isDocumentFragment(shadowNode.content)) {
1356
1466
  _sanitizeShadowDOM2(shadowNode.content);
1357
1467
  }
1358
1468
  }
@@ -1376,21 +1486,42 @@ function createDOMPurify() {
1376
1486
  * existing _sanitizeShadowDOM template-content recursion) stay
1377
1487
  * untouched — string-input paths are not affected.
1378
1488
  *
1489
+ * DOM-Clobbering hardening: HTMLFormElement carries the WebIDL
1490
+ * [LegacyOverrideBuiltIns] extended attribute, so a descendant element
1491
+ * named `nodeType`, `shadowRoot`, or `childNodes` shadows the matching
1492
+ * prototype getter on the form. Reading those properties directly off
1493
+ * the node would let an attacker steer this walk past shadow hosts
1494
+ * (e.g. <input name="childNodes"> collapses the form's child list to
1495
+ * the input itself, so descent stops dead and any shadow root deeper
1496
+ * in the subtree is never sanitized). Every property access here is
1497
+ * therefore routed through the cached prototype getter; the form's
1498
+ * named-property getter cannot intercept those reads.
1499
+ *
1379
1500
  * @param root the subtree root to walk for attached shadow roots
1380
1501
  */
1381
1502
  const _sanitizeAttachedShadowRoots2 = function _sanitizeAttachedShadowRoots(root) {
1382
- if (root.nodeType === NODE_TYPE.element && root.shadowRoot instanceof DocumentFragment) {
1383
- const sr = root.shadowRoot;
1384
- // Recurse first so that nested shadow roots are reached even if
1385
- // _sanitizeShadowDOM removes hosts at this level.
1386
- _sanitizeAttachedShadowRoots2(sr);
1387
- _sanitizeShadowDOM2(sr);
1503
+ const nodeType = getNodeType ? getNodeType(root) : root.nodeType;
1504
+ if (nodeType === NODE_TYPE.element) {
1505
+ const sr = getShadowRoot ? getShadowRoot(root) : root.shadowRoot;
1506
+ // Realm-safe check (GHSA-hpcv-96wg-7vj8): use nodeType-based
1507
+ // detection rather than `instanceof DocumentFragment`, which is
1508
+ // realm-bound and silently skipped shadow roots whose host element
1509
+ // belonged to a foreign realm (e.g. iframe.contentDocument
1510
+ // attachShadow). A foreign-realm ShadowRoot extends the foreign
1511
+ // realm's DocumentFragment, not ours, so the old instanceof check
1512
+ // returned false and the shadow subtree was never walked.
1513
+ if (_isDocumentFragment(sr)) {
1514
+ // Recurse first so that nested shadow roots are reached even if
1515
+ // _sanitizeShadowDOM removes hosts at this level.
1516
+ _sanitizeAttachedShadowRoots2(sr);
1517
+ _sanitizeShadowDOM2(sr);
1518
+ }
1388
1519
  }
1389
1520
  // Snapshot children before recursing. Sanitization of one subtree
1390
1521
  // (e.g. via an uponSanitizeShadowNode hook) may detach siblings,
1391
1522
  // and naive nextSibling traversal would silently skip the rest of
1392
1523
  // the list once a node is detached.
1393
- const childNodes = root.childNodes;
1524
+ const childNodes = getChildNodes ? getChildNodes(root) : root.childNodes;
1394
1525
  if (!childNodes) {
1395
1526
  return;
1396
1527
  }
@@ -1438,14 +1569,31 @@ function createDOMPurify() {
1438
1569
  IN_PLACE = false;
1439
1570
  }
1440
1571
  if (IN_PLACE) {
1441
- /* Do some early pre-sanitization to avoid unsafe root nodes */
1442
- const nn = dirty.nodeName;
1572
+ /* Do some early pre-sanitization to avoid unsafe root nodes.
1573
+ Read nodeName through the cached prototype getter — a clobbering
1574
+ child named "nodeName" on the form root would otherwise shadow
1575
+ the property and let this check skip the root-allowlist
1576
+ validation entirely. */
1577
+ const nn = getNodeName ? getNodeName(dirty) : dirty.nodeName;
1443
1578
  if (typeof nn === 'string') {
1444
1579
  const tagName = transformCaseFunc(nn);
1445
1580
  if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {
1446
1581
  throw typeErrorCreate('root node is forbidden and cannot be sanitized in-place');
1447
1582
  }
1448
1583
  }
1584
+ /* Pre-flight the root through _isClobbered. The iterator-driven
1585
+ removal path can not detach a parent-less root: _forceRemove
1586
+ falls through to Element.prototype.remove(), which per spec
1587
+ is a no-op on a node with no parent. A clobbered root would
1588
+ then survive the main loop with its attributes uninspected,
1589
+ because _sanitizeAttributes early-returns on _isClobbered. The
1590
+ result would be an attacker-controlled form, complete with any
1591
+ event-handler attributes the caller passed in, handed back to
1592
+ the application unsanitized. Refuse to sanitize such a root
1593
+ the same way we refuse a forbidden tag. GHSA-r47g-fvhr-h676. */
1594
+ if (_isClobbered(dirty)) {
1595
+ throw typeErrorCreate('root node is clobbered and cannot be sanitized in-place');
1596
+ }
1449
1597
  /* Sanitize attached shadow roots before the main iterator runs.
1450
1598
  The iterator does not descend into shadow trees. */
1451
1599
  _sanitizeAttachedShadowRoots2(dirty);
@@ -1465,7 +1613,9 @@ function createDOMPurify() {
1465
1613
  }
1466
1614
  /* Clonable shadow roots are deep-cloned by importNode(); sanitize
1467
1615
  them before the main iterator runs, since the iterator does not
1468
- descend into shadow trees. */
1616
+ descend into shadow trees. The walk routes every read through a
1617
+ cached prototype getter so clobbering descendants on a form root
1618
+ cannot hide a shadow host from this pass. */
1469
1619
  _sanitizeAttachedShadowRoots2(importedNode);
1470
1620
  } else {
1471
1621
  /* Exit directly if we have nothing to do */
@@ -1493,8 +1643,11 @@ function createDOMPurify() {
1493
1643
  _sanitizeElements(currentNode);
1494
1644
  /* Check attributes next */
1495
1645
  _sanitizeAttributes(currentNode);
1496
- /* Shadow DOM detected, sanitize it */
1497
- if (currentNode.content instanceof DocumentFragment) {
1646
+ /* Shadow DOM detected, sanitize it.
1647
+ Realm-safe check (GHSA-hpcv-96wg-7vj8): nodeType-based detection
1648
+ instead of instanceof, so foreign-realm <template>.content is
1649
+ walked correctly. */
1650
+ if (_isDocumentFragment(currentNode.content)) {
1498
1651
  _sanitizeShadowDOM2(currentNode.content);
1499
1652
  }
1500
1653
  }