dompurify 3.4.12 → 3.4.14

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.12 | (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.12/LICENSE */
1
+ /*! @license DOMPurify 3.4.14 | (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.14/LICENSE */
2
2
 
3
3
  'use strict';
4
4
 
@@ -313,7 +313,7 @@ const mathMlDisallowed = freeze(['maction', 'maligngroup', 'malignmark', 'mlongd
313
313
  const text = freeze(['#text']);
314
314
 
315
315
  const html = freeze(['accept', 'action', 'align', 'alt', 'autocapitalize', 'autocomplete', 'autopictureinpicture', 'autoplay', 'background', 'bgcolor', 'border', 'capture', 'cellpadding', 'cellspacing', 'checked', 'cite', 'class', 'clear', 'color', 'cols', 'colspan', 'command', 'commandfor', 'controls', 'controlslist', 'coords', 'crossorigin', 'datetime', 'decoding', 'default', 'dir', 'disabled', 'disablepictureinpicture', 'disableremoteplayback', 'download', 'draggable', 'enctype', 'enterkeyhint', 'exportparts', 'face', 'for', 'headers', 'height', 'hidden', 'high', 'href', 'hreflang', 'id', 'inert', 'inputmode', 'integrity', 'ismap', 'kind', 'label', 'lang', 'list', 'loading', 'loop', 'low', 'max', 'maxlength', 'media', 'method', 'min', 'minlength', 'multiple', 'muted', 'name', 'nonce', 'noshade', 'novalidate', 'nowrap', 'open', 'optimum', 'part', 'pattern', 'placeholder', 'playsinline', 'popover', 'popovertarget', 'popovertargetaction', 'poster', 'preload', 'pubdate', 'radiogroup', 'readonly', 'rel', 'required', 'rev', 'reversed', 'role', 'rows', 'rowspan', 'spellcheck', 'scope', 'selected', 'shape', 'size', 'sizes', 'slot', 'span', 'srclang', 'start', 'src', 'srcset', 'step', 'style', 'summary', 'tabindex', 'title', 'translate', 'type', 'usemap', 'valign', 'value', 'width', 'wrap', 'xmlns']);
316
- const svg = freeze(['accent-height', 'accumulate', 'additive', 'alignment-baseline', 'amplitude', 'ascent', 'attributename', 'attributetype', 'azimuth', 'basefrequency', 'baseline-shift', 'begin', 'bias', 'by', 'class', 'clip', 'clippathunits', 'clip-path', 'clip-rule', 'color', 'color-interpolation', 'color-interpolation-filters', 'color-profile', 'color-rendering', 'cx', 'cy', 'd', 'dx', 'dy', 'diffuseconstant', 'direction', 'display', 'divisor', 'dominant-baseline', 'dur', 'edgemode', 'elevation', 'end', 'exponent', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'filterunits', 'flood-color', 'flood-opacity', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'fx', 'fy', 'g1', 'g2', 'glyph-name', 'glyphref', 'gradientunits', 'gradienttransform', 'height', 'href', 'id', 'image-rendering', 'in', 'in2', 'intercept', 'k', 'k1', 'k2', 'k3', 'k4', 'kerning', 'keypoints', 'keysplines', 'keytimes', 'lang', 'lengthadjust', 'letter-spacing', 'kernelmatrix', 'kernelunitlength', 'lighting-color', 'local', 'marker-end', 'marker-mid', 'marker-start', 'markerheight', 'markerunits', 'markerwidth', 'maskcontentunits', 'maskunits', 'max', 'mask', 'mask-type', 'media', 'method', 'mode', 'min', 'name', 'numoctaves', 'offset', 'operator', 'opacity', 'order', 'orient', 'orientation', 'origin', 'overflow', 'paint-order', 'path', 'pathlength', 'patterncontentunits', 'patterntransform', 'patternunits', 'points', 'preservealpha', 'preserveaspectratio', 'primitiveunits', 'r', 'rx', 'ry', 'radius', 'refx', 'refy', 'repeatcount', 'repeatdur', 'restart', 'result', 'rotate', 'scale', 'seed', 'shape-rendering', 'slope', 'specularconstant', 'specularexponent', 'spreadmethod', 'startoffset', 'stddeviation', 'stitchtiles', 'stop-color', 'stop-opacity', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke', 'stroke-width', 'style', 'surfacescale', 'systemlanguage', 'tabindex', 'tablevalues', 'targetx', 'targety', 'transform', 'transform-origin', 'text-anchor', 'text-decoration', 'text-orientation', 'text-rendering', 'textlength', 'type', 'u1', 'u2', 'unicode', 'values', 'viewbox', 'visibility', 'version', 'vert-adv-y', 'vert-origin-x', 'vert-origin-y', 'width', 'word-spacing', 'wrap', 'writing-mode', 'xchannelselector', 'ychannelselector', 'x', 'x1', 'x2', 'xmlns', 'y', 'y1', 'y2', 'z', 'zoomandpan']);
316
+ const svg = freeze(['accent-height', 'accumulate', 'additive', 'alignment-baseline', 'amplitude', 'ascent', 'attributename', 'attributetype', 'azimuth', 'basefrequency', 'baseline-shift', 'begin', 'bias', 'by', 'class', 'clip', 'clippathunits', 'clip-path', 'clip-rule', 'color', 'color-interpolation', 'color-interpolation-filters', 'color-profile', 'color-rendering', 'cx', 'cy', 'd', 'dx', 'dy', 'diffuseconstant', 'direction', 'display', 'divisor', 'dominant-baseline', 'dur', 'edgemode', 'elevation', 'end', 'exponent', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'filterunits', 'flood-color', 'flood-opacity', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'fx', 'fy', 'g1', 'g2', 'glyph-name', 'glyphref', 'gradientunits', 'gradienttransform', 'height', 'href', 'id', 'image-rendering', 'in', 'in2', 'intercept', 'k', 'k1', 'k2', 'k3', 'k4', 'kerning', 'keypoints', 'keysplines', 'keytimes', 'lang', 'lengthadjust', 'letter-spacing', 'kernelmatrix', 'kernelunitlength', 'lighting-color', 'local', 'marker-end', 'marker-mid', 'marker-start', 'markerheight', 'markerunits', 'markerwidth', 'maskcontentunits', 'maskunits', 'max', 'mask', 'mask-type', 'media', 'method', 'mode', 'min', 'name', 'numoctaves', 'offset', 'operator', 'opacity', 'order', 'orient', 'orientation', 'origin', 'overflow', 'paint-order', 'path', 'pathlength', 'patterncontentunits', 'patterntransform', 'patternunits', 'pointer-events', 'points', 'preservealpha', 'preserveaspectratio', 'primitiveunits', 'r', 'rx', 'ry', 'radius', 'refx', 'refy', 'repeatcount', 'repeatdur', 'restart', 'result', 'rotate', 'scale', 'seed', 'shape-rendering', 'slope', 'specularconstant', 'specularexponent', 'spreadmethod', 'startoffset', 'stddeviation', 'stitchtiles', 'stop-color', 'stop-opacity', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke', 'stroke-width', 'style', 'surfacescale', 'systemlanguage', 'tabindex', 'tablevalues', 'targetx', 'targety', 'transform', 'transform-origin', 'text-anchor', 'text-decoration', 'text-orientation', 'text-rendering', 'textlength', 'type', 'u1', 'u2', 'unicode', 'values', 'vector-effect', 'viewbox', 'visibility', 'version', 'vert-adv-y', 'vert-origin-x', 'vert-origin-y', 'width', 'word-spacing', 'wrap', 'writing-mode', 'xchannelselector', 'ychannelselector', 'x', 'x1', 'x2', 'xmlns', 'y', 'y1', 'y2', 'z', 'zoomandpan']);
317
317
  const mathMl = freeze(['accent', 'accentunder', 'align', 'bevelled', 'close', 'columnalign', 'columnlines', 'columnspacing', 'columnspan', 'denomalign', 'depth', 'dir', 'display', 'displaystyle', 'encoding', 'fence', 'frame', 'height', 'href', 'id', 'largeop', 'length', 'linethickness', 'lquote', 'lspace', 'mathbackground', 'mathcolor', 'mathsize', 'mathvariant', 'maxsize', 'minsize', 'movablelimits', 'notation', 'numalign', 'open', 'rowalign', 'rowlines', 'rowspacing', 'rowspan', 'rspace', 'rquote', 'scriptlevel', 'scriptminsize', 'scriptsizemultiplier', 'selection', 'separator', 'separators', 'stretchy', 'subscriptshift', 'supscriptshift', 'symmetric', 'voffset', 'width', 'xmlns']);
318
318
  const xml = freeze(['xlink:href', 'xml:id', 'xlink:title', 'xml:space', 'xmlns:xlink']);
319
319
 
@@ -354,6 +354,37 @@ const NODE_TYPE = {
354
354
  documentFragment: 11,
355
355
  notation: 12 // Deprecated
356
356
  };
357
+ /* HTML-namespace elements whose child text nodes are serialized *literally*
358
+ (unescaped) by the HTML fragment-serialization algorithm. Two reparse-mXSS
359
+ shapes ride on that literal serialization:
360
+ (a) an element child - a tree the HTML parser can never build, but the DOM
361
+ API and an XML/XHTML parse can - after which a `</tag>`-bearing text
362
+ sibling breaks the element open on reparse; and
363
+ (b) text-only content that already carries the element's OWN end tag, e.g.
364
+ `<style>...</style><img onerror=x>` built as a node, which the literal
365
+ serializer emits verbatim for the HTML parser to re-open.
366
+ Shape (a) is handled by the firstElementChild branch in _isUnsafeNode; shape
367
+ (b) by the LITERAL_TEXT_CLOSE probe. Both read textContent (the raw-serialized
368
+ form for these elements) rather than innerHTML, because an XML/XHTML working
369
+ document serializes innerHTML with `<` escaped, which silently blinds the
370
+ innerHTML-based probes (rule 1's second probe and FALLBACK_TAG_CLOSE) there.
371
+ `script` is never allow-listed, but is kept here so the guard matches the
372
+ serializer's own literal-text list exactly. */
373
+ const LITERAL_TEXT_ELEMENT_NAMES = ['style', 'script', 'xmp', 'iframe', 'noembed', 'noframes', 'plaintext', 'noscript'];
374
+ const LITERAL_TEXT_ELEMENTS = freeze(addToSet({}, LITERAL_TEXT_ELEMENT_NAMES));
375
+ /* Per-element end-tag matcher. On an HTML reparse the ONLY token that
376
+ terminates a literal-text element's raw content is its own end tag; a foreign
377
+ literal-text close (e.g. `</xmp>` sitting inside `<style>`) does not break
378
+ out, so matching is per-element, not a shared alternation. The lookahead
379
+ requires an HTML tag-name terminator (whitespace, `/` or `>`) so a longer
380
+ name such as `</styles` is not mistaken for `</style`. */
381
+ const LITERAL_TEXT_CLOSE = function () {
382
+ const map = {};
383
+ arrayForEach(LITERAL_TEXT_ELEMENT_NAMES, name => {
384
+ map[name] = seal(new RegExp('</' + name + '(?=[\\t\\n\\f\\r />])', 'i'));
385
+ });
386
+ return freeze(map);
387
+ }();
357
388
  const getGlobal = function getGlobal() {
358
389
  return typeof window === 'undefined' ? null : window;
359
390
  };
@@ -423,10 +454,25 @@ const _createHooksMap = function _createHooksMap() {
423
454
  const _resolveSetOption = function _resolveSetOption(cfg, key, fallback, options) {
424
455
  return objectHasOwnProperty(cfg, key) && arrayIsArray(cfg[key]) ? addToSet(options.base ? clone(options.base) : {}, cfg[key], options.transform) : fallback;
425
456
  };
457
+ /**
458
+ * Resolve an object-valued configuration option: a prototype-free clone
459
+ * of cfg[key] when it is an own, truthy object property, else a fresh
460
+ * fallback built by makeFallback (fresh on every parse, so a previous
461
+ * parse can never leak state into the next one).
462
+ *
463
+ * @param cfg the cloned, prototype-free configuration object
464
+ * @param key the configuration property to read
465
+ * @param makeFallback builds the fallback value when the option is absent
466
+ * @returns the resolved object
467
+ */
468
+ const _resolveObjectOption = function _resolveObjectOption(cfg, key, makeFallback) {
469
+ const value = objectHasOwnProperty(cfg, key) ? cfg[key] : undefined;
470
+ return value && typeof value === 'object' ? clone(value) : makeFallback();
471
+ };
426
472
  function createDOMPurify() {
427
473
  let window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal();
428
474
  const DOMPurify = root => createDOMPurify(root);
429
- DOMPurify.version = '3.4.12';
475
+ DOMPurify.version = '3.4.14';
430
476
  DOMPurify.removed = [];
431
477
  if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) {
432
478
  // Not running in a browser, provide a factory function
@@ -457,6 +503,17 @@ function createDOMPurify() {
457
503
  const getAttributes = lookupGetter(ElementPrototype, 'attributes');
458
504
  const getNodeType = Node && Node.prototype ? lookupGetter(Node.prototype, 'nodeType') : null;
459
505
  const getNodeName = Node && Node.prototype ? lookupGetter(Node.prototype, 'nodeName') : null;
506
+ const getOwnerDocument = Node && Node.prototype ? lookupGetter(Node.prototype, 'ownerDocument') : null;
507
+ /* Clobber-safe nodeType / nodeName reads through the cached Node.prototype
508
+ getters, with a direct-property fallback for environments that lack
509
+ Node.prototype. Sites that need a different fallback (e.g. _isClobbered
510
+ returns early on a null name) intentionally keep their own reads. */
511
+ const _readNodeType = function _readNodeType(node) {
512
+ return getNodeType ? getNodeType(node) : node.nodeType;
513
+ };
514
+ const _readNodeName = function _readNodeName(node) {
515
+ return getNodeName ? getNodeName(node) : node.nodeName;
516
+ };
460
517
  // As per issue #47, the web-components registry is inherited by a
461
518
  // new document created via createHTMLDocument. As per the spec
462
519
  // (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries)
@@ -779,9 +836,11 @@ function createDOMPurify() {
779
836
  IN_PLACE = cfg.IN_PLACE || false; // Default false
780
837
  IS_ALLOWED_URI$1 = isRegex(cfg.ALLOWED_URI_REGEXP) ? cfg.ALLOWED_URI_REGEXP : IS_ALLOWED_URI; // Default regexp
781
838
  NAMESPACE = typeof cfg.NAMESPACE === 'string' ? cfg.NAMESPACE : HTML_NAMESPACE; // Default HTML namespace
782
- MATHML_TEXT_INTEGRATION_POINTS = objectHasOwnProperty(cfg, 'MATHML_TEXT_INTEGRATION_POINTS') && cfg.MATHML_TEXT_INTEGRATION_POINTS && typeof cfg.MATHML_TEXT_INTEGRATION_POINTS === 'object' ? clone(cfg.MATHML_TEXT_INTEGRATION_POINTS) : addToSet({}, DEFAULT_MATHML_TEXT_INTEGRATION_POINTS); // Default built-in map
783
- HTML_INTEGRATION_POINTS = objectHasOwnProperty(cfg, 'HTML_INTEGRATION_POINTS') && cfg.HTML_INTEGRATION_POINTS && typeof cfg.HTML_INTEGRATION_POINTS === 'object' ? clone(cfg.HTML_INTEGRATION_POINTS) : addToSet({}, DEFAULT_HTML_INTEGRATION_POINTS); // Default built-in map
784
- const customElementHandling = objectHasOwnProperty(cfg, 'CUSTOM_ELEMENT_HANDLING') && cfg.CUSTOM_ELEMENT_HANDLING && typeof cfg.CUSTOM_ELEMENT_HANDLING === 'object' ? clone(cfg.CUSTOM_ELEMENT_HANDLING) : create(null);
839
+ MATHML_TEXT_INTEGRATION_POINTS = _resolveObjectOption(cfg, 'MATHML_TEXT_INTEGRATION_POINTS', () => addToSet({}, DEFAULT_MATHML_TEXT_INTEGRATION_POINTS) // Default built-in map
840
+ );
841
+ HTML_INTEGRATION_POINTS = _resolveObjectOption(cfg, 'HTML_INTEGRATION_POINTS', () => addToSet({}, DEFAULT_HTML_INTEGRATION_POINTS) // Default built-in map
842
+ );
843
+ const customElementHandling = _resolveObjectOption(cfg, 'CUSTOM_ELEMENT_HANDLING', () => create(null));
785
844
  CUSTOM_ELEMENT_HANDLING = create(null);
786
845
  if (objectHasOwnProperty(customElementHandling, 'tagNameCheck') && isRegexOrFunction(customElementHandling.tagNameCheck)) {
787
846
  CUSTOM_ELEMENT_HANDLING.tagNameCheck = customElementHandling.tagNameCheck; // Default undefined
@@ -848,15 +907,6 @@ function createDOMPurify() {
848
907
  addToSet(ALLOWED_ATTR, cfg.ADD_ATTR, transformCaseFunc);
849
908
  }
850
909
  }
851
- if (objectHasOwnProperty(cfg, 'ADD_URI_SAFE_ATTR') && arrayIsArray(cfg.ADD_URI_SAFE_ATTR)) {
852
- addToSet(URI_SAFE_ATTRIBUTES, cfg.ADD_URI_SAFE_ATTR, transformCaseFunc);
853
- }
854
- if (objectHasOwnProperty(cfg, 'FORBID_CONTENTS') && arrayIsArray(cfg.FORBID_CONTENTS)) {
855
- if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) {
856
- FORBID_CONTENTS = clone(FORBID_CONTENTS);
857
- }
858
- addToSet(FORBID_CONTENTS, cfg.FORBID_CONTENTS, transformCaseFunc);
859
- }
860
910
  if (objectHasOwnProperty(cfg, 'ADD_FORBID_CONTENTS') && arrayIsArray(cfg.ADD_FORBID_CONTENTS)) {
861
911
  if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) {
862
912
  FORBID_CONTENTS = clone(FORBID_CONTENTS);
@@ -1091,6 +1141,32 @@ function createDOMPurify() {
1091
1141
  }
1092
1142
  }
1093
1143
  };
1144
+ /**
1145
+ * _stripAttributeNode
1146
+ *
1147
+ * Remove a single Attr node case/namespace-exactly on an attribute-teardown
1148
+ * path. Name-based removeAttribute() ASCII-lowercases its lookup key for an
1149
+ * HTML element in an HTML document and so silently misses a case-preserved
1150
+ * handler (e.g. `ONERROR` off an XML/XHTML import) - the same defect
1151
+ * _removeAttribute() was fixed for, which a name-based call would reintroduce
1152
+ * on these IN_PLACE teardown paths. Unlike _removeAttribute this does not
1153
+ * record into DOMPurify.removed: the neutralize passes intentionally do not
1154
+ * book-keep. A clobbered/detached node falls back to best-effort name-based
1155
+ * removal.
1156
+ *
1157
+ * @param element the element to strip the attribute from
1158
+ * @param attribute the Attr node to remove
1159
+ * @param name the attribute's name, for the fallback path
1160
+ */
1161
+ const _stripAttributeNode = function _stripAttributeNode(element, attribute, name) {
1162
+ try {
1163
+ element.removeAttributeNode(attribute);
1164
+ } catch (_) {
1165
+ try {
1166
+ element.removeAttribute(name);
1167
+ } catch (_) {}
1168
+ }
1169
+ };
1094
1170
  /**
1095
1171
  * _neutralizeRoot
1096
1172
  *
@@ -1135,11 +1211,7 @@ function createDOMPurify() {
1135
1211
  const attribute = attributes[i];
1136
1212
  const name = attribute && attribute.name;
1137
1213
  if (typeof name === 'string') {
1138
- try {
1139
- root.removeAttribute(name);
1140
- } catch (_) {
1141
- /* Clobbered removeAttribute — ignore (fail-closed best effort) */
1142
- }
1214
+ _stripAttributeNode(root, attribute, name);
1143
1215
  }
1144
1216
  }
1145
1217
  }
@@ -1147,22 +1219,46 @@ function createDOMPurify() {
1147
1219
  /**
1148
1220
  * _removeAttribute
1149
1221
  *
1222
+ * Name-based getAttributeNode()/removeAttribute() ASCII-lowercase their
1223
+ * lookup key for HTML elements in an HTML document, so they silently miss an
1224
+ * attribute whose stored qualified name still contains uppercase ASCII
1225
+ * letters. That happens when the node came from a case-preserving source
1226
+ * (an XML/XHTML document imported via importNode(), or createAttributeNS()),
1227
+ * where e.g. `ONERROR` survives the walk: the policy check lowercases to
1228
+ * `onerror` and rejects it, but `removeAttribute('ONERROR')` looks up
1229
+ * `onerror` and finds nothing. Remove the exact Attr node instead, which is
1230
+ * case- and namespace-exact, and fall back to name-based removal only when
1231
+ * the caller could not supply the node.
1232
+ *
1150
1233
  * @param name an Attribute name
1151
1234
  * @param element a DOM node
1235
+ * @param attr the exact Attr node to remove, when the caller has it
1152
1236
  */
1153
- const _removeAttribute = function _removeAttribute(name, element) {
1237
+ const _removeAttribute = function _removeAttribute(name, element, attr) {
1238
+ if (!attr) {
1239
+ try {
1240
+ attr = element.getAttributeNode(name);
1241
+ } catch (_) {
1242
+ attr = null;
1243
+ }
1244
+ }
1245
+ arrayPush(DOMPurify.removed, {
1246
+ attribute: attr || null,
1247
+ from: element
1248
+ });
1154
1249
  try {
1155
- arrayPush(DOMPurify.removed, {
1156
- attribute: element.getAttributeNode(name),
1157
- from: element
1158
- });
1250
+ if (attr) {
1251
+ element.removeAttributeNode(attr);
1252
+ } else {
1253
+ element.removeAttribute(name);
1254
+ }
1159
1255
  } catch (_) {
1160
- arrayPush(DOMPurify.removed, {
1161
- attribute: null,
1162
- from: element
1163
- });
1256
+ /* Clobbered or already-detached node - best-effort fall back to a
1257
+ name-based removal so the "is" handling below still runs. */
1258
+ try {
1259
+ element.removeAttribute(name);
1260
+ } catch (_) {}
1164
1261
  }
1165
- element.removeAttribute(name);
1166
1262
  // We void attribute values for unremovable "is" attributes
1167
1263
  if (name === 'is') {
1168
1264
  if (RETURN_DOM || RETURN_DOM_FRAGMENT) {
@@ -1197,11 +1293,7 @@ function createDOMPurify() {
1197
1293
  if (typeof name !== 'string' || ALLOWED_ATTR[transformCaseFunc(name)]) {
1198
1294
  continue;
1199
1295
  }
1200
- try {
1201
- element.removeAttribute(name);
1202
- } catch (_) {
1203
- /* Clobbered removeAttribute on a doomed node — ignore */
1204
- }
1296
+ _stripAttributeNode(element, attribute, name);
1205
1297
  }
1206
1298
  };
1207
1299
  /**
@@ -1230,7 +1322,7 @@ function createDOMPurify() {
1230
1322
  const stack = [root];
1231
1323
  while (stack.length > 0) {
1232
1324
  const node = stack.pop();
1233
- const nodeType = getNodeType ? getNodeType(node) : node.nodeType;
1325
+ const nodeType = _readNodeType(node);
1234
1326
  if (nodeType === NODE_TYPE.element) {
1235
1327
  _stripDisallowedAttributes(node);
1236
1328
  }
@@ -1277,6 +1369,26 @@ function createDOMPurify() {
1277
1369
  *
1278
1370
  * @param root the in-place root to sweep
1279
1371
  */
1372
+ /**
1373
+ * Central policy for declarative-partial-updates patch-linkage attributes,
1374
+ * shared by the _neutralizePatchLinkage pre-pass and _isValidAttribute so
1375
+ * the two sites cannot drift: `patchsrc` always links, `for` links
1376
+ * everywhere except on <label>/<output>, and the whole policy is gated on
1377
+ * SAFE_FOR_XML (see the rationale block in _isValidAttribute).
1378
+ *
1379
+ * @param lcName the transformCaseFunc'd attribute name
1380
+ * @param lcTag the transformCaseFunc'd tag name of the carrying element
1381
+ * @return true if the attribute is patch linkage and must be dropped
1382
+ */
1383
+ const _isPatchLinkageAttribute = function _isPatchLinkageAttribute(lcName, lcTag) {
1384
+ if (!SAFE_FOR_XML) {
1385
+ return false;
1386
+ }
1387
+ if (lcName === 'patchsrc') {
1388
+ return true;
1389
+ }
1390
+ return lcName === 'for' && lcTag !== 'label' && lcTag !== 'output';
1391
+ };
1280
1392
  const _neutralizePatchLinkage = function _neutralizePatchLinkage(root) {
1281
1393
  if (!SAFE_FOR_XML) {
1282
1394
  return;
@@ -1284,7 +1396,7 @@ function createDOMPurify() {
1284
1396
  const stack = [root];
1285
1397
  while (stack.length > 0) {
1286
1398
  const node = stack.pop();
1287
- const nodeType = getNodeType ? getNodeType(node) : node.nodeType;
1399
+ const nodeType = _readNodeType(node);
1288
1400
  /* Remove range markers (the target side of a patch linkage): every
1289
1401
  processing instruction, and any markup-bearing comment. */
1290
1402
  if (nodeType === NODE_TYPE.processingInstruction || nodeType === NODE_TYPE.comment && regExpTest(COMMENT_MARKUP_PROBE, node.data)) {
@@ -1298,12 +1410,12 @@ function createDOMPurify() {
1298
1410
  /* Strip patch-source attributes (the source side) off elements. */
1299
1411
  if (nodeType === NODE_TYPE.element) {
1300
1412
  const element = node;
1301
- const lcTag = transformCaseFunc(getNodeName ? getNodeName(node) : node.nodeName);
1413
+ const lcTag = transformCaseFunc(_readNodeName(node));
1302
1414
  try {
1303
1415
  if (element.hasAttribute && element.hasAttribute('patchsrc')) {
1304
1416
  element.removeAttribute('patchsrc');
1305
1417
  }
1306
- if (element.hasAttribute && element.hasAttribute('for') && lcTag !== 'label' && lcTag !== 'output') {
1418
+ if (element.hasAttribute && element.hasAttribute('for') && _isPatchLinkageAttribute('for', lcTag)) {
1307
1419
  element.removeAttribute('for');
1308
1420
  }
1309
1421
  } catch (_) {
@@ -1375,7 +1487,17 @@ function createDOMPurify() {
1375
1487
  * @return The created NodeIterator
1376
1488
  */
1377
1489
  const _createNodeIterator = function _createNodeIterator(root) {
1378
- return createNodeIterator.call(root.ownerDocument || root, root,
1490
+ /* Read ownerDocument through the cached Node.prototype getter, never the
1491
+ direct property. HTMLFormElement has [LegacyOverrideBuiltIns], so a
1492
+ clobbering child (<input name="ownerDocument"> or a form-associated
1493
+ external input) shadows the prototype getter and makes a direct read
1494
+ return that <input>. createNodeIterator.call(<input>, ...) then throws
1495
+ "Illegal invocation", and on the IN_PLACE path that throw lands before
1496
+ the walk's fail-closed barrier - leaving the caller's live tree, with
1497
+ any already-armed handler in it, un-neutralized. The cached getter
1498
+ returns the real Document regardless of the clobber. */
1499
+ const doc = getOwnerDocument ? getOwnerDocument(root) : root.ownerDocument;
1500
+ return createNodeIterator.call(doc || root, root,
1379
1501
  // eslint-disable-next-line no-bitwise
1380
1502
  NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_PROCESSING_INSTRUCTION | NodeFilter.SHOW_CDATA_SECTION, null);
1381
1503
  };
@@ -1415,7 +1537,11 @@ function createDOMPurify() {
1415
1537
  const _scrubTemplateExpressions2 = function _scrubTemplateExpressions(node) {
1416
1538
  var _node$querySelectorAl;
1417
1539
  node.normalize();
1418
- const walker = createNodeIterator.call(node.ownerDocument || node, node,
1540
+ /* Clobber-safe ownerDocument read, same reasoning as _createNodeIterator:
1541
+ under SAFE_FOR_TEMPLATES this runs on the live IN_PLACE root, which may
1542
+ carry a form-named-getter override of ownerDocument. */
1543
+ const doc = getOwnerDocument ? getOwnerDocument(node) : node.ownerDocument;
1544
+ const walker = createNodeIterator.call(doc || node, node,
1419
1545
  // eslint-disable-next-line no-bitwise
1420
1546
  NodeFilter.SHOW_TEXT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_CDATA_SECTION | NodeFilter.SHOW_PROCESSING_INSTRUCTION, null);
1421
1547
  let currentNode = walker.nextNode();
@@ -1551,8 +1677,16 @@ function createDOMPurify() {
1551
1677
  if (SAFE_FOR_XML && currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.textContent) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.innerHTML)) {
1552
1678
  return true;
1553
1679
  }
1554
- /* Remove risky CSS construction leading to mXSS */
1555
- if (SAFE_FOR_XML && currentNode.namespaceURI === HTML_NAMESPACE && tagName === 'style' && _isNode(currentNode.firstElementChild)) {
1680
+ /* Remove rawtext/literal-text elements whose literal serialization
1681
+ re-opens markup on an HTML reparse - shapes (a) and (b) documented at
1682
+ LITERAL_TEXT_ELEMENTS. Both are invisible to rule 1 above (which
1683
+ self-disables once there is an element child, and whose second probe
1684
+ reads the innerHTML an XML/XHTML document serializes escaped), which
1685
+ is why both probes here read textContent instead. Previously only
1686
+ `style`-with-element-child was covered; every element in
1687
+ LITERAL_TEXT_ELEMENTS shares this literal serialization and is
1688
+ equally affected. */
1689
+ if (SAFE_FOR_XML && currentNode.namespaceURI === HTML_NAMESPACE && LITERAL_TEXT_ELEMENTS[tagName] && (_isNode(currentNode.firstElementChild) || typeof currentNode.textContent === 'string' && regExpTest(LITERAL_TEXT_CLOSE[tagName], currentNode.textContent))) {
1556
1690
  return true;
1557
1691
  }
1558
1692
  /* Remove any occurrence of processing instructions */
@@ -1565,6 +1699,30 @@ function createDOMPurify() {
1565
1699
  }
1566
1700
  return false;
1567
1701
  };
1702
+ /**
1703
+ * Evaluate a CUSTOM_ELEMENT_HANDLING check (a RegExp or a predicate
1704
+ * function, per the validation in _parseConfig) against a name.
1705
+ * Additional arguments are forwarded to predicate functions - the
1706
+ * attributeNameCheck predicate receives the tag name as its second
1707
+ * argument. A null/absent check never matches.
1708
+ *
1709
+ * @param check the configured tagNameCheck / attributeNameCheck value
1710
+ * @param name the name to test
1711
+ * @param args extra arguments forwarded to a predicate function
1712
+ * @return true if the check matches the name
1713
+ */
1714
+ const _matchesNameCheck = function _matchesNameCheck(check, name) {
1715
+ if (check instanceof RegExp) {
1716
+ return regExpTest(check, name);
1717
+ }
1718
+ if (check instanceof Function) {
1719
+ for (var _len = arguments.length, args = new Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) {
1720
+ args[_key - 2] = arguments[_key];
1721
+ }
1722
+ return Boolean(check(name, ...args));
1723
+ }
1724
+ return false;
1725
+ };
1568
1726
  /**
1569
1727
  * Handle a node whose tag is forbidden or not allowlisted: keep
1570
1728
  * allowed custom elements (false return exits _sanitizeElements
@@ -1582,15 +1740,10 @@ function createDOMPurify() {
1582
1740
  * @param tagName the node's transformCaseFunc'd tag name
1583
1741
  * @return true if the node was removed, false if kept
1584
1742
  */
1585
- const _sanitizeDisallowedNode = function _sanitizeDisallowedNode(currentNode, tagName) {
1743
+ const _sanitizeDisallowedNode = function _sanitizeDisallowedNode(currentNode, tagName, root) {
1586
1744
  /* Check if we have a custom element to handle */
1587
- if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {
1588
- if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) {
1589
- return false;
1590
- }
1591
- if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)) {
1592
- return false;
1593
- }
1745
+ if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName) && _matchesNameCheck(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) {
1746
+ return false;
1594
1747
  }
1595
1748
  /* Keep content except for bad-listed elements.
1596
1749
  Use the cached prototype getters exclusively — the previous code
@@ -1605,27 +1758,26 @@ function createDOMPurify() {
1605
1758
  const childNodes = getChildNodes(currentNode);
1606
1759
  if (childNodes && parentNode) {
1607
1760
  const childCount = childNodes.length;
1608
- /* In-place: hoist the *original* children so the iterator visits
1609
- and sanitises them through the same allowlist pass as every other
1610
- node. The caller built the tree in the live document, so the
1611
- originals carry already-queued resource events (`<img onerror>`,
1612
- `<video>`/`<audio>` error, lazy/`onload`, …); cloning would leave
1613
- those originals detached but still armed, firing in page scope
1614
- while the returned tree looked clean. Moving is safe in-place: the
1615
- root is pre-validated as an allowed tag and so is never the node
1616
- being removed, which keeps `parentNode` inside the iterator root
1617
- and the relocated child inside the serialised tree.
1618
- Otherwise (string / DOM-copy paths): clone. The iterator is rooted
1619
- at and the result serialised from `body`, so a restrictive
1620
- ALLOWED_TAGS that removes `body` itself must leave its content in
1621
- place, which only cloning does; and those paths parse into an
1622
- inert document, so their discarded originals never had a queued
1623
- event to neutralise.
1761
+ /* Hoist by moving each child up one level rather than deep-cloning
1762
+ it. Moving transfers every descendant exactly once, so a chain of
1763
+ nested disallowed elements costs O(n) instead of the O(n^2) that
1764
+ re-cloning the shrinking subtree at each level produced; it also
1765
+ empties the removed original, so `DOMPurify.removed` no longer
1766
+ pins whole subtrees. Moving preserves the in-place guarantee too:
1767
+ an original carrying already-queued resource events (`<img
1768
+ onerror>`, `<video>`/`<audio>` error, lazy/`onload`, …) is
1769
+ relocated and sanitised rather than left detached but still armed.
1770
+ The sole case that must clone is removing the walk root itself.
1771
+ The result is serialised from the root's subtree, so a restrictive
1772
+ ALLOWED_TAGS that strips the root (`body` on the string path) must
1773
+ leave the content inside it, which only cloning does. In IN_PLACE
1774
+ the root is pre-validated as an allowed tag and so is never removed
1775
+ here, so that path always takes the move branch.
1624
1776
  `childNodes` is live; a tail-to-head walk keeps `childNodes[i]`
1625
1777
  valid whether we move (drops the trailing entry) or clone (leaves
1626
1778
  the list intact). */
1627
1779
  for (let i = childCount - 1; i >= 0; --i) {
1628
- const hoisted = IN_PLACE ? childNodes[i] : cloneNode(childNodes[i], true);
1780
+ const hoisted = currentNode === root ? cloneNode(childNodes[i], true) : childNodes[i];
1629
1781
  parentNode.insertBefore(hoisted, getNextSibling(currentNode));
1630
1782
  }
1631
1783
  }
@@ -1633,6 +1785,66 @@ function createDOMPurify() {
1633
1785
  _forceRemove(currentNode);
1634
1786
  return true;
1635
1787
  };
1788
+ /**
1789
+ * Fork a hook-mutable allowlist off its shared binding the first time a
1790
+ * (possibly lazily-installed) uponSanitize* hook is about to see it, so the
1791
+ * hook cannot widen the per-instance default or the setConfig binding by
1792
+ * reference and leak past the call. Returns the set unchanged once it is
1793
+ * already call-local, so repeated calls across elements are idempotent.
1794
+ *
1795
+ * @param hookList the uponSanitize* hook array for this event
1796
+ * @param set the current ALLOWED_TAGS / ALLOWED_ATTR binding
1797
+ * @param defaultSet the per-instance DEFAULT_ALLOWED_* constant
1798
+ * @param setConfigSet the captured setConfig() binding, or null
1799
+ * @return a call-local clone if a hook is present and set is still shared,
1800
+ * else set unchanged
1801
+ */
1802
+ const _forkSharedAllowlist = function _forkSharedAllowlist(hookList, set, defaultSet, setConfigSet) {
1803
+ if (hookList.length === 0) {
1804
+ return set;
1805
+ }
1806
+ return set === defaultSet || set === setConfigSet ? clone(set) : set;
1807
+ };
1808
+ /**
1809
+ * Shared guard for a node that a hook has detached from the walk tree,
1810
+ * used after each element-hook site in _sanitizeElements. Detaching is a
1811
+ * long-standing user pattern (issue #469; draw.io-style foreignObject
1812
+ * filtering). Per the cached, unclobberable parentNode getter the node is
1813
+ * genuinely out of the tree, so it can reach neither the serialized
1814
+ * output nor an IN_PLACE live tree; treat it as removed and stop
1815
+ * processing it. Without this guard, the unsafe-node / namespace checks
1816
+ * would call _forceRemove on a parentless node and hit the REPORT-3
1817
+ * fail-closed throw — which exists for nodes DOMPurify wants gone but
1818
+ * *cannot* detach (clobbered / parentless roots), the opposite of a node
1819
+ * that is already safely gone. The walk root is exempt: a detached
1820
+ * IN_PLACE root is legitimate input and must still be fully sanitized,
1821
+ * and a kill-decision on it must keep hitting the REPORT-3 throw.
1822
+ *
1823
+ * Nodes detached by hooks stay the hook's responsibility for placement:
1824
+ * they are not recorded in DOMPurify.removed, so the post-walk IN_PLACE
1825
+ * pass (which iterates DOMPurify.removed) does not reach them. But a
1826
+ * hook-detached subtree can still hold a queued resource-event handler -
1827
+ * e.g. an <img onload> that began loading when the caller built the live
1828
+ * tree - which fires in page scope after sanitize returns even though the
1829
+ * handler never reached the returned tree. That is the audit-5 F1 hazard,
1830
+ * and the documented node.remove() hook pattern walks straight into it.
1831
+ * So on the IN_PLACE path we neutralize the detached subtree inline,
1832
+ * stripping its non-allow-listed attributes before returning, exactly as
1833
+ * the post-walk pass does for _forceRemove'd subtrees.
1834
+ *
1835
+ * @param currentNode the node a hook may have detached
1836
+ * @param root the current walk root
1837
+ * @return true if the node is detached and now handled, false otherwise
1838
+ */
1839
+ const _handleHookDetachedNode = function _handleHookDetachedNode(currentNode, root) {
1840
+ if (currentNode === root || getParentNode(currentNode) !== null) {
1841
+ return false;
1842
+ }
1843
+ if (IN_PLACE) {
1844
+ _neutralizeSubtree(currentNode);
1845
+ }
1846
+ return true;
1847
+ };
1636
1848
  /**
1637
1849
  * _sanitizeElements
1638
1850
  *
@@ -1642,13 +1854,12 @@ function createDOMPurify() {
1642
1854
  * @param currentNode to check for permission to exist
1643
1855
  * @return true if node was killed, false if left alive
1644
1856
  */
1645
- // eslint-disable-next-line complexity
1646
1857
  const _sanitizeElements = function _sanitizeElements(currentNode, root) {
1647
1858
  /* Execute a hook if present */
1648
1859
  _executeHooks(hooks.beforeSanitizeElements, currentNode, null);
1649
- /* A hook may have detached the node treat it as removed (see the
1650
- detached-node comment after the uponSanitizeElement hook below). */
1651
- if (currentNode !== root && getParentNode(currentNode) === null) {
1860
+ /* A hook may have detached the node - treat it as removed (see
1861
+ _handleHookDetachedNode for the full rationale). */
1862
+ if (_handleHookDetachedNode(currentNode, root)) {
1652
1863
  return true;
1653
1864
  }
1654
1865
  /* Check if element is clobbered or can clobber */
@@ -1657,28 +1868,21 @@ function createDOMPurify() {
1657
1868
  return true;
1658
1869
  }
1659
1870
  /* Now let's check the element's type and name */
1660
- const tagName = transformCaseFunc(getNodeName ? getNodeName(currentNode) : currentNode.nodeName);
1871
+ const tagName = transformCaseFunc(_readNodeName(currentNode));
1872
+ /* Close the pre-walk clone-guard's timing gap: an uponSanitizeElement
1873
+ hook may have been installed after that guard sampled the hook arrays
1874
+ (e.g. lazily from beforeSanitizeElements), leaving ALLOWED_TAGS still
1875
+ aliasing a shared binding that a widening hook would mutate by
1876
+ reference. Fork it before exposing it to the hook. */
1877
+ ALLOWED_TAGS = _forkSharedAllowlist(hooks.uponSanitizeElement, ALLOWED_TAGS, DEFAULT_ALLOWED_TAGS, SET_CONFIG_ALLOWED_TAGS);
1661
1878
  /* Execute a hook if present */
1662
1879
  _executeHooks(hooks.uponSanitizeElement, currentNode, {
1663
1880
  tagName,
1664
1881
  allowedTags: ALLOWED_TAGS
1665
1882
  });
1666
- /* A hook may have detached the node from the tree — a long-standing
1667
- user pattern (issue #469; draw.io-style foreignObject filtering).
1668
- Per the cached, unclobberable parentNode getter the node is
1669
- genuinely out of the tree, so it can reach neither the serialized
1670
- output nor an IN_PLACE live tree; treat it as removed and stop
1671
- processing it. Without this guard, the unsafe-node / namespace
1672
- checks below would call _forceRemove on a parentless node and hit
1673
- the REPORT-3 fail-closed throw — which exists for nodes DOMPurify
1674
- wants gone but *cannot* detach (clobbered / parentless roots), the
1675
- opposite of a node that is already safely gone. The walk root is
1676
- exempt: a detached IN_PLACE root is legitimate input and must still
1677
- be fully sanitized, and a kill-decision on it must keep hitting the
1678
- REPORT-3 throw. Nodes detached by hooks are the hook's
1679
- responsibility: they are not recorded in DOMPurify.removed and are
1680
- not neutralized by the post-walk IN_PLACE pass. */
1681
- if (currentNode !== root && getParentNode(currentNode) === null) {
1883
+ /* The uponSanitizeElement hook may have detached the node, exactly as
1884
+ above (see _handleHookDetachedNode for the full rationale). */
1885
+ if (_handleHookDetachedNode(currentNode, root)) {
1682
1886
  return true;
1683
1887
  }
1684
1888
  /* Remove mXSS vectors, processing instructions and risky comments */
@@ -1688,7 +1892,7 @@ function createDOMPurify() {
1688
1892
  }
1689
1893
  /* Remove element if anything forbids its presence */
1690
1894
  if (FORBID_TAGS[tagName] || !(EXTRA_ELEMENT_HANDLING.tagCheck instanceof Function && EXTRA_ELEMENT_HANDLING.tagCheck(tagName)) && !ALLOWED_TAGS[tagName]) {
1691
- const removed = _sanitizeDisallowedNode(currentNode, tagName);
1895
+ const removed = _sanitizeDisallowedNode(currentNode, tagName, root);
1692
1896
  /* A false return means the node is a custom element kept via
1693
1897
  CUSTOM_ELEMENT_HANDLING - the only keep path through
1694
1898
  _sanitizeDisallowedNode. Run afterSanitizeElements on it so the
@@ -1711,7 +1915,7 @@ function createDOMPurify() {
1711
1915
  bound and short-circuits to false for any node minted in a different
1712
1916
  realm — letting a foreign-realm element with a forbidden namespace
1713
1917
  slip past the namespace check entirely. */
1714
- const nt = getNodeType ? getNodeType(currentNode) : currentNode.nodeType;
1918
+ const nt = _readNodeType(currentNode);
1715
1919
  if (nt === NODE_TYPE.element && !_checkValidNamespace(currentNode)) {
1716
1920
  _forceRemove(currentNode);
1717
1921
  return true;
@@ -1771,10 +1975,7 @@ function createDOMPurify() {
1771
1975
  other structural-threat checks and stays overridable, consistent with
1772
1976
  the rest of the codebase. PI range markers are already removed by
1773
1977
  _isUnsafeNode. */
1774
- if (SAFE_FOR_XML && lcName === 'patchsrc') {
1775
- return false;
1776
- }
1777
- if (SAFE_FOR_XML && lcName === 'for' && lcTag !== 'label' && lcTag !== 'output') {
1978
+ if (_isPatchLinkageAttribute(lcName, lcTag)) {
1778
1979
  return false;
1779
1980
  }
1780
1981
  /* Make sure attribute cannot clobber */
@@ -1786,22 +1987,54 @@ function createDOMPurify() {
1786
1987
  (https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes)
1787
1988
  XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804)
1788
1989
  We don't need to check the value; it's always URI safe. */
1789
- if (ALLOW_DATA_ATTR && regExpTest(DATA_ATTR$1, lcName)) ; else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR$1, lcName)) ; else if (!nameIsPermitted) {
1790
- if (
1791
- // First condition does a very basic check if a) it's basically a valid custom element tagname AND
1792
- // b) if the tagName passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck
1793
- // and c) if the attribute name passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.attributeNameCheck
1794
- _isBasicCustomElement(lcTag) && (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, lcTag) || CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(lcTag)) && (CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.attributeNameCheck, lcName) || CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.attributeNameCheck(lcName, lcTag)) ||
1795
- // Alternative, second condition checks if it's an `is`-attribute, AND
1796
- // the value passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck
1797
- lcName === 'is' && CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements && (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, value) || CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(value))) ; else {
1798
- return false;
1799
- }
1800
- /* Check value is safe. First, is attr inert? If so, is safe */
1801
- } else if (URI_SAFE_ATTRIBUTES[lcName]) ; else if (regExpTest(IS_ALLOWED_URI$1, stringReplace(value, ATTR_WHITESPACE$1, ''))) ; else if ((lcName === 'src' || lcName === 'xlink:href' || lcName === 'href') && lcTag !== 'script' && stringIndexOf(value, 'data:') === 0 && DATA_URI_TAGS[lcTag]) ; else if (ALLOW_UNKNOWN_PROTOCOLS && !regExpTest(IS_SCRIPT_OR_DATA$1, stringReplace(value, ATTR_WHITESPACE$1, ''))) ; else if (value) {
1802
- return false;
1803
- } else ;
1804
- return true;
1990
+ if (ALLOW_DATA_ATTR && regExpTest(DATA_ATTR$1, lcName)) {
1991
+ return true;
1992
+ }
1993
+ /* Allow valid aria-* attributes, the value is always URI safe */
1994
+ if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR$1, lcName)) {
1995
+ return true;
1996
+ }
1997
+ /* A name outside the allowlist is acceptable on custom-element terms
1998
+ only. The value checks below are intentionally skipped in that case:
1999
+ if the user supplied a tagNameCheck we also allow derived custom
2000
+ elements using the same test, and attributes passing the configured
2001
+ attributeNameCheck are allowed as custom elements define these at
2002
+ their own discretion. */
2003
+ if (!nameIsPermitted) {
2004
+ return (
2005
+ // Condition a) covers a basically valid custom element tag name whose
2006
+ // tag passes the configured tagNameCheck and whose attribute name
2007
+ // passes the configured attributeNameCheck ...
2008
+ _isBasicCustomElement(lcTag) && _matchesNameCheck(CUSTOM_ELEMENT_HANDLING.tagNameCheck, lcTag) && _matchesNameCheck(CUSTOM_ELEMENT_HANDLING.attributeNameCheck, lcName, lcTag) ||
2009
+ // Condition b) covers an `is` attribute whose value passes the
2010
+ // configured tagNameCheck while customized built-in elements are
2011
+ // allowed.
2012
+ lcName === 'is' && CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements && _matchesNameCheck(CUSTOM_ELEMENT_HANDLING.tagNameCheck, value)
2013
+ );
2014
+ }
2015
+ /* Check value is safe. First, is attr inert? If so, is safe */
2016
+ if (URI_SAFE_ATTRIBUTES[lcName]) {
2017
+ return true;
2018
+ }
2019
+ /* Check no script, data or unknown possibly unsafe URI
2020
+ unless we know URI values are safe for that attribute */
2021
+ if (regExpTest(IS_ALLOWED_URI$1, stringReplace(value, ATTR_WHITESPACE$1, ''))) {
2022
+ return true;
2023
+ }
2024
+ /* Keep image data URIs alive if src/xlink:href is allowed */
2025
+ /* Further prevent gadget XSS for dynamically built script tags */
2026
+ if ((lcName === 'src' || lcName === 'xlink:href' || lcName === 'href') && lcTag !== 'script' && stringIndexOf(value, 'data:') === 0 && DATA_URI_TAGS[lcTag]) {
2027
+ return true;
2028
+ }
2029
+ /* Allow unknown protocols: This provides support for links that
2030
+ are handled by protocol handlers which may be unknown ahead of
2031
+ time, e.g. fb:, spotify: */
2032
+ if (ALLOW_UNKNOWN_PROTOCOLS && !regExpTest(IS_SCRIPT_OR_DATA$1, stringReplace(value, ATTR_WHITESPACE$1, ''))) {
2033
+ return true;
2034
+ }
2035
+ /* Only an empty (binary) value remains safe at this point;
2036
+ anything else is presumed unsafe, do not add it back */
2037
+ return !value;
1805
2038
  };
1806
2039
  /* Names the HTML spec reserves from valid-custom-element-name; these must
1807
2040
  * never be treated as basic custom elements even when a permissive
@@ -1893,6 +2126,9 @@ function createDOMPurify() {
1893
2126
  if (!attributes || _isClobbered(currentNode)) {
1894
2127
  return;
1895
2128
  }
2129
+ /* Same lazy-install guard as uponSanitizeElement (see there): fork the
2130
+ attribute allowlist off its shared binding before a hook can see it. */
2131
+ ALLOWED_ATTR = _forkSharedAllowlist(hooks.uponSanitizeAttribute, ALLOWED_ATTR, DEFAULT_ALLOWED_ATTR, SET_CONFIG_ALLOWED_ATTR);
1896
2132
  const hookEvent = {
1897
2133
  attrName: '',
1898
2134
  attrValue: '',
@@ -1923,7 +2159,7 @@ function createDOMPurify() {
1923
2159
  */
1924
2160
  if (SANITIZE_NAMED_PROPS && (lcName === 'id' || lcName === 'name') && stringIndexOf(value, SANITIZE_NAMED_PROPS_PREFIX) !== 0) {
1925
2161
  // Remove the attribute with this value
1926
- _removeAttribute(name, currentNode);
2162
+ _removeAttribute(name, currentNode, attr);
1927
2163
  // Prefix the value and later re-create the attribute with the sanitized value
1928
2164
  value = SANITIZE_NAMED_PROPS_PREFIX + value;
1929
2165
  }
@@ -1931,12 +2167,12 @@ function createDOMPurify() {
1931
2167
  // itself the clobbering protection, and re-applying it is incorrect.
1932
2168
  /* Work around a security issue with comments inside attributes */
1933
2169
  if (SAFE_FOR_XML && regExpTest(/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i, value)) {
1934
- _removeAttribute(name, currentNode);
2170
+ _removeAttribute(name, currentNode, attr);
1935
2171
  continue;
1936
2172
  }
1937
2173
  /* Make sure we cannot easily use animated hrefs, even if animations are allowed */
1938
2174
  if (lcName === 'attributename' && stringMatch(value, 'href')) {
1939
- _removeAttribute(name, currentNode);
2175
+ _removeAttribute(name, currentNode, attr);
1940
2176
  continue;
1941
2177
  }
1942
2178
  /* Did the hooks force-keep the attribute? */
@@ -1945,12 +2181,12 @@ function createDOMPurify() {
1945
2181
  }
1946
2182
  /* Did the hooks approve of the attribute? */
1947
2183
  if (!hookEvent.keepAttr) {
1948
- _removeAttribute(name, currentNode);
2184
+ _removeAttribute(name, currentNode, attr);
1949
2185
  continue;
1950
2186
  }
1951
2187
  /* Work around a security issue in jQuery 3.0 */
1952
2188
  if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(SELF_CLOSING_TAG, value)) {
1953
- _removeAttribute(name, currentNode);
2189
+ _removeAttribute(name, currentNode, attr);
1954
2190
  continue;
1955
2191
  }
1956
2192
  /* Sanitize attribute content to be template-safe */
@@ -1959,7 +2195,7 @@ function createDOMPurify() {
1959
2195
  }
1960
2196
  /* Is `value` valid for this attribute? */
1961
2197
  if (!_isValidAttribute(lcTag, lcName, value)) {
1962
- _removeAttribute(name, currentNode);
2198
+ _removeAttribute(name, currentNode, attr);
1963
2199
  continue;
1964
2200
  }
1965
2201
  /* Handle attributes that require Trusted Types */
@@ -2006,8 +2242,7 @@ function createDOMPurify() {
2006
2242
  Walk it explicitly. The nodeType guard avoids reading
2007
2243
  shadowRoot off text / comment / CDATA / PI nodes that the
2008
2244
  iterator also surfaces. */
2009
- const shadowNodeType = getNodeType ? getNodeType(shadowNode) : shadowNode.nodeType;
2010
- if (shadowNodeType === NODE_TYPE.element) {
2245
+ if (_readNodeType(shadowNode) === NODE_TYPE.element) {
2011
2246
  const innerSr = getShadowRoot(shadowNode);
2012
2247
  if (_isDocumentFragment(innerSr)) {
2013
2248
  _sanitizeAttachedShadowRoots(innerSr);
@@ -2065,7 +2300,7 @@ function createDOMPurify() {
2065
2300
  continue;
2066
2301
  }
2067
2302
  const node = item.node;
2068
- const nodeType = getNodeType ? getNodeType(node) : node.nodeType;
2303
+ const nodeType = _readNodeType(node);
2069
2304
  const isElement = nodeType === NODE_TYPE.element;
2070
2305
  /* (pushed last → processed first) Children, snapshotted in reverse so
2071
2306
  the first child is processed first. Snapshotting matters because a
@@ -2187,7 +2422,7 @@ function createDOMPurify() {
2187
2422
  child named "nodeName" on the form root would otherwise shadow
2188
2423
  the property and let this check skip the root-allowlist
2189
2424
  validation entirely. */
2190
- const nn = getNodeName ? getNodeName(dirty) : dirty.nodeName;
2425
+ const nn = _readNodeName(dirty);
2191
2426
  if (typeof nn === 'string') {
2192
2427
  const tagName = transformCaseFunc(nn);
2193
2428
  if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {
@@ -2265,17 +2500,20 @@ function createDOMPurify() {
2265
2500
  }
2266
2501
  /* Get node iterator */
2267
2502
  const walkRoot = inPlace ? dirty : body;
2268
- const nodeIterator = _createNodeIterator(walkRoot);
2269
2503
  /* Now start iterating over the created document.
2270
2504
  The walk runs inside an exception barrier (campaign-3 F2): a re-entrant
2271
2505
  engine/custom-element mutation can detach a node mid-walk so
2272
2506
  `_forceRemove`'s parentless guard throws, aborting the loop. Without the
2273
2507
  barrier the caller's in-place tree would be left half-sanitized with the
2274
- unvisited tail still armed. On any throw we fail closed — strip the
2275
- in-place root bare then rethrow so the existing throw contract is
2276
- preserved. (String/DOM-copy paths never return the partial body, so the
2277
- propagating throw is already fail-closed there.) */
2508
+ unvisited tail still armed. _createNodeIterator itself is inside the
2509
+ barrier too: constructing the iterator dereferences the root's document,
2510
+ and any failure there (e.g. an exotic/clobbered root) must still fail
2511
+ closed rather than skip the neutralize. On any throw we fail closed -
2512
+ strip the in-place root bare - then rethrow so the existing throw
2513
+ contract is preserved. (String/DOM-copy paths never return the partial
2514
+ body, so the propagating throw is already fail-closed there.) */
2278
2515
  try {
2516
+ const nodeIterator = _createNodeIterator(walkRoot);
2279
2517
  while (currentNode = nodeIterator.nextNode()) {
2280
2518
  /* Sanitize tags and elements */
2281
2519
  _sanitizeElements(currentNode, walkRoot);