dompurify 3.4.13 → 3.4.15
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/README.md +10 -2
- package/dist/purify.cjs.d.ts +1 -1
- package/dist/purify.cjs.js +360 -126
- package/dist/purify.cjs.js.map +1 -1
- package/dist/purify.es.d.mts +1 -1
- package/dist/purify.es.mjs +360 -126
- package/dist/purify.es.mjs.map +1 -1
- package/dist/purify.js +360 -126
- package/dist/purify.js.map +1 -1
- package/dist/purify.min.js +2 -2
- package/dist/purify.min.js.map +1 -1
- package/package.json +1 -1
- package/src/attrs.ts +2 -0
- package/src/purify.ts +442 -210
- package/src/types.ts +2 -5
package/dist/purify.es.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! @license DOMPurify 3.4.
|
|
1
|
+
/*! @license DOMPurify 3.4.15 | (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.15/LICENSE */
|
|
2
2
|
|
|
3
3
|
function _arrayLikeToArray(r, a) {
|
|
4
4
|
(null == a || a > r.length) && (a = r.length);
|
|
@@ -311,7 +311,7 @@ const mathMlDisallowed = freeze(['maction', 'maligngroup', 'malignmark', 'mlongd
|
|
|
311
311
|
const text = freeze(['#text']);
|
|
312
312
|
|
|
313
313
|
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']);
|
|
314
|
-
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']);
|
|
314
|
+
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']);
|
|
315
315
|
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']);
|
|
316
316
|
const xml = freeze(['xlink:href', 'xml:id', 'xlink:title', 'xml:space', 'xmlns:xlink']);
|
|
317
317
|
|
|
@@ -352,6 +352,37 @@ const NODE_TYPE = {
|
|
|
352
352
|
documentFragment: 11,
|
|
353
353
|
notation: 12 // Deprecated
|
|
354
354
|
};
|
|
355
|
+
/* HTML-namespace elements whose child text nodes are serialized *literally*
|
|
356
|
+
(unescaped) by the HTML fragment-serialization algorithm. Two reparse-mXSS
|
|
357
|
+
shapes ride on that literal serialization:
|
|
358
|
+
(a) an element child - a tree the HTML parser can never build, but the DOM
|
|
359
|
+
API and an XML/XHTML parse can - after which a `</tag>`-bearing text
|
|
360
|
+
sibling breaks the element open on reparse; and
|
|
361
|
+
(b) text-only content that already carries the element's OWN end tag, e.g.
|
|
362
|
+
`<style>...</style><img onerror=x>` built as a node, which the literal
|
|
363
|
+
serializer emits verbatim for the HTML parser to re-open.
|
|
364
|
+
Shape (a) is handled by the firstElementChild branch in _isUnsafeNode; shape
|
|
365
|
+
(b) by the LITERAL_TEXT_CLOSE probe. Both read textContent (the raw-serialized
|
|
366
|
+
form for these elements) rather than innerHTML, because an XML/XHTML working
|
|
367
|
+
document serializes innerHTML with `<` escaped, which silently blinds the
|
|
368
|
+
innerHTML-based probes (rule 1's second probe and FALLBACK_TAG_CLOSE) there.
|
|
369
|
+
`script` is never allow-listed, but is kept here so the guard matches the
|
|
370
|
+
serializer's own literal-text list exactly. */
|
|
371
|
+
const LITERAL_TEXT_ELEMENT_NAMES = ['style', 'script', 'xmp', 'iframe', 'noembed', 'noframes', 'plaintext', 'noscript'];
|
|
372
|
+
const LITERAL_TEXT_ELEMENTS = freeze(addToSet({}, LITERAL_TEXT_ELEMENT_NAMES));
|
|
373
|
+
/* Per-element end-tag matcher. On an HTML reparse the ONLY token that
|
|
374
|
+
terminates a literal-text element's raw content is its own end tag; a foreign
|
|
375
|
+
literal-text close (e.g. `</xmp>` sitting inside `<style>`) does not break
|
|
376
|
+
out, so matching is per-element, not a shared alternation. The lookahead
|
|
377
|
+
requires an HTML tag-name terminator (whitespace, `/` or `>`) so a longer
|
|
378
|
+
name such as `</styles` is not mistaken for `</style`. */
|
|
379
|
+
const LITERAL_TEXT_CLOSE = function () {
|
|
380
|
+
const map = {};
|
|
381
|
+
arrayForEach(LITERAL_TEXT_ELEMENT_NAMES, name => {
|
|
382
|
+
map[name] = seal(new RegExp('</' + name + '(?=[\\t\\n\\f\\r />])', 'i'));
|
|
383
|
+
});
|
|
384
|
+
return freeze(map);
|
|
385
|
+
}();
|
|
355
386
|
const getGlobal = function getGlobal() {
|
|
356
387
|
return typeof window === 'undefined' ? null : window;
|
|
357
388
|
};
|
|
@@ -421,10 +452,25 @@ const _createHooksMap = function _createHooksMap() {
|
|
|
421
452
|
const _resolveSetOption = function _resolveSetOption(cfg, key, fallback, options) {
|
|
422
453
|
return objectHasOwnProperty(cfg, key) && arrayIsArray(cfg[key]) ? addToSet(options.base ? clone(options.base) : {}, cfg[key], options.transform) : fallback;
|
|
423
454
|
};
|
|
455
|
+
/**
|
|
456
|
+
* Resolve an object-valued configuration option: a prototype-free clone
|
|
457
|
+
* of cfg[key] when it is an own, truthy object property, else a fresh
|
|
458
|
+
* fallback built by makeFallback (fresh on every parse, so a previous
|
|
459
|
+
* parse can never leak state into the next one).
|
|
460
|
+
*
|
|
461
|
+
* @param cfg the cloned, prototype-free configuration object
|
|
462
|
+
* @param key the configuration property to read
|
|
463
|
+
* @param makeFallback builds the fallback value when the option is absent
|
|
464
|
+
* @returns the resolved object
|
|
465
|
+
*/
|
|
466
|
+
const _resolveObjectOption = function _resolveObjectOption(cfg, key, makeFallback) {
|
|
467
|
+
const value = objectHasOwnProperty(cfg, key) ? cfg[key] : undefined;
|
|
468
|
+
return value && typeof value === 'object' ? clone(value) : makeFallback();
|
|
469
|
+
};
|
|
424
470
|
function createDOMPurify() {
|
|
425
471
|
let window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal();
|
|
426
472
|
const DOMPurify = root => createDOMPurify(root);
|
|
427
|
-
DOMPurify.version = '3.4.
|
|
473
|
+
DOMPurify.version = '3.4.15';
|
|
428
474
|
DOMPurify.removed = [];
|
|
429
475
|
if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) {
|
|
430
476
|
// Not running in a browser, provide a factory function
|
|
@@ -448,6 +494,12 @@ function createDOMPurify() {
|
|
|
448
494
|
const ElementPrototype = Element.prototype;
|
|
449
495
|
const cloneNode = lookupGetter(ElementPrototype, 'cloneNode');
|
|
450
496
|
const remove = lookupGetter(ElementPrototype, 'remove');
|
|
497
|
+
// Clobber-safe Attr-node removal. On an HTMLFormElement a descendant named
|
|
498
|
+
// "removeAttributeNode" shadows the prototype method via
|
|
499
|
+
// [LegacyOverrideBuiltIns], so element.removeAttributeNode(attr) throws.
|
|
500
|
+
// Calling the cached Element.prototype method with the element as the
|
|
501
|
+
// receiver removes the exact live Attr node regardless of the shadowing.
|
|
502
|
+
const removeAttributeNode = lookupGetter(ElementPrototype, 'removeAttributeNode');
|
|
451
503
|
const getNextSibling = lookupGetter(ElementPrototype, 'nextSibling');
|
|
452
504
|
const getChildNodes = lookupGetter(ElementPrototype, 'childNodes');
|
|
453
505
|
const getParentNode = lookupGetter(ElementPrototype, 'parentNode');
|
|
@@ -456,6 +508,16 @@ function createDOMPurify() {
|
|
|
456
508
|
const getNodeType = Node && Node.prototype ? lookupGetter(Node.prototype, 'nodeType') : null;
|
|
457
509
|
const getNodeName = Node && Node.prototype ? lookupGetter(Node.prototype, 'nodeName') : null;
|
|
458
510
|
const getOwnerDocument = Node && Node.prototype ? lookupGetter(Node.prototype, 'ownerDocument') : null;
|
|
511
|
+
/* Clobber-safe nodeType / nodeName reads through the cached Node.prototype
|
|
512
|
+
getters, with a direct-property fallback for environments that lack
|
|
513
|
+
Node.prototype. Sites that need a different fallback (e.g. _isClobbered
|
|
514
|
+
returns early on a null name) intentionally keep their own reads. */
|
|
515
|
+
const _readNodeType = function _readNodeType(node) {
|
|
516
|
+
return getNodeType ? getNodeType(node) : node.nodeType;
|
|
517
|
+
};
|
|
518
|
+
const _readNodeName = function _readNodeName(node) {
|
|
519
|
+
return getNodeName ? getNodeName(node) : node.nodeName;
|
|
520
|
+
};
|
|
459
521
|
// As per issue #47, the web-components registry is inherited by a
|
|
460
522
|
// new document created via createHTMLDocument. As per the spec
|
|
461
523
|
// (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries)
|
|
@@ -778,9 +840,11 @@ function createDOMPurify() {
|
|
|
778
840
|
IN_PLACE = cfg.IN_PLACE || false; // Default false
|
|
779
841
|
IS_ALLOWED_URI$1 = isRegex(cfg.ALLOWED_URI_REGEXP) ? cfg.ALLOWED_URI_REGEXP : IS_ALLOWED_URI; // Default regexp
|
|
780
842
|
NAMESPACE = typeof cfg.NAMESPACE === 'string' ? cfg.NAMESPACE : HTML_NAMESPACE; // Default HTML namespace
|
|
781
|
-
MATHML_TEXT_INTEGRATION_POINTS =
|
|
782
|
-
|
|
783
|
-
|
|
843
|
+
MATHML_TEXT_INTEGRATION_POINTS = _resolveObjectOption(cfg, 'MATHML_TEXT_INTEGRATION_POINTS', () => addToSet({}, DEFAULT_MATHML_TEXT_INTEGRATION_POINTS) // Default built-in map
|
|
844
|
+
);
|
|
845
|
+
HTML_INTEGRATION_POINTS = _resolveObjectOption(cfg, 'HTML_INTEGRATION_POINTS', () => addToSet({}, DEFAULT_HTML_INTEGRATION_POINTS) // Default built-in map
|
|
846
|
+
);
|
|
847
|
+
const customElementHandling = _resolveObjectOption(cfg, 'CUSTOM_ELEMENT_HANDLING', () => create(null));
|
|
784
848
|
CUSTOM_ELEMENT_HANDLING = create(null);
|
|
785
849
|
if (objectHasOwnProperty(customElementHandling, 'tagNameCheck') && isRegexOrFunction(customElementHandling.tagNameCheck)) {
|
|
786
850
|
CUSTOM_ELEMENT_HANDLING.tagNameCheck = customElementHandling.tagNameCheck; // Default undefined
|
|
@@ -847,15 +911,6 @@ function createDOMPurify() {
|
|
|
847
911
|
addToSet(ALLOWED_ATTR, cfg.ADD_ATTR, transformCaseFunc);
|
|
848
912
|
}
|
|
849
913
|
}
|
|
850
|
-
if (objectHasOwnProperty(cfg, 'ADD_URI_SAFE_ATTR') && arrayIsArray(cfg.ADD_URI_SAFE_ATTR)) {
|
|
851
|
-
addToSet(URI_SAFE_ATTRIBUTES, cfg.ADD_URI_SAFE_ATTR, transformCaseFunc);
|
|
852
|
-
}
|
|
853
|
-
if (objectHasOwnProperty(cfg, 'FORBID_CONTENTS') && arrayIsArray(cfg.FORBID_CONTENTS)) {
|
|
854
|
-
if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) {
|
|
855
|
-
FORBID_CONTENTS = clone(FORBID_CONTENTS);
|
|
856
|
-
}
|
|
857
|
-
addToSet(FORBID_CONTENTS, cfg.FORBID_CONTENTS, transformCaseFunc);
|
|
858
|
-
}
|
|
859
914
|
if (objectHasOwnProperty(cfg, 'ADD_FORBID_CONTENTS') && arrayIsArray(cfg.ADD_FORBID_CONTENTS)) {
|
|
860
915
|
if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) {
|
|
861
916
|
FORBID_CONTENTS = clone(FORBID_CONTENTS);
|
|
@@ -1090,6 +1145,32 @@ function createDOMPurify() {
|
|
|
1090
1145
|
}
|
|
1091
1146
|
}
|
|
1092
1147
|
};
|
|
1148
|
+
/**
|
|
1149
|
+
* _stripAttributeNode
|
|
1150
|
+
*
|
|
1151
|
+
* Remove a single Attr node case/namespace-exactly on an attribute-teardown
|
|
1152
|
+
* path. Name-based removeAttribute() ASCII-lowercases its lookup key for an
|
|
1153
|
+
* HTML element in an HTML document and so silently misses a case-preserved
|
|
1154
|
+
* handler (e.g. `ONERROR` off an XML/XHTML import) - the same defect
|
|
1155
|
+
* _removeAttribute() was fixed for, which a name-based call would reintroduce
|
|
1156
|
+
* on these IN_PLACE teardown paths. Unlike _removeAttribute this does not
|
|
1157
|
+
* record into DOMPurify.removed: the neutralize passes intentionally do not
|
|
1158
|
+
* book-keep. A clobbered/detached node falls back to best-effort name-based
|
|
1159
|
+
* removal.
|
|
1160
|
+
*
|
|
1161
|
+
* @param element the element to strip the attribute from
|
|
1162
|
+
* @param attribute the Attr node to remove
|
|
1163
|
+
* @param name the attribute's name, for the fallback path
|
|
1164
|
+
*/
|
|
1165
|
+
const _stripAttributeNode = function _stripAttributeNode(element, attribute, name) {
|
|
1166
|
+
try {
|
|
1167
|
+
removeAttributeNode(element, attribute);
|
|
1168
|
+
} catch (_) {
|
|
1169
|
+
try {
|
|
1170
|
+
element.removeAttribute(name);
|
|
1171
|
+
} catch (_) {}
|
|
1172
|
+
}
|
|
1173
|
+
};
|
|
1093
1174
|
/**
|
|
1094
1175
|
* _neutralizeRoot
|
|
1095
1176
|
*
|
|
@@ -1134,11 +1215,7 @@ function createDOMPurify() {
|
|
|
1134
1215
|
const attribute = attributes[i];
|
|
1135
1216
|
const name = attribute && attribute.name;
|
|
1136
1217
|
if (typeof name === 'string') {
|
|
1137
|
-
|
|
1138
|
-
root.removeAttribute(name);
|
|
1139
|
-
} catch (_) {
|
|
1140
|
-
/* Clobbered removeAttribute — ignore (fail-closed best effort) */
|
|
1141
|
-
}
|
|
1218
|
+
_stripAttributeNode(root, attribute, name);
|
|
1142
1219
|
}
|
|
1143
1220
|
}
|
|
1144
1221
|
}
|
|
@@ -1146,22 +1223,51 @@ function createDOMPurify() {
|
|
|
1146
1223
|
/**
|
|
1147
1224
|
* _removeAttribute
|
|
1148
1225
|
*
|
|
1226
|
+
* Name-based getAttributeNode()/removeAttribute() ASCII-lowercase their
|
|
1227
|
+
* lookup key for HTML elements in an HTML document, so they silently miss an
|
|
1228
|
+
* attribute whose stored qualified name still contains uppercase ASCII
|
|
1229
|
+
* letters. That happens when the node came from a case-preserving source
|
|
1230
|
+
* (an XML/XHTML document imported via importNode(), or createAttributeNS()),
|
|
1231
|
+
* where e.g. `ONERROR` survives the walk: the policy check lowercases to
|
|
1232
|
+
* `onerror` and rejects it, but `removeAttribute('ONERROR')` looks up
|
|
1233
|
+
* `onerror` and finds nothing. Remove the exact Attr node instead, which is
|
|
1234
|
+
* case- and namespace-exact, and fall back to name-based removal only when
|
|
1235
|
+
* the caller could not supply the node.
|
|
1236
|
+
*
|
|
1149
1237
|
* @param name an Attribute name
|
|
1150
1238
|
* @param element a DOM node
|
|
1239
|
+
* @param attr the exact Attr node to remove, when the caller has it
|
|
1151
1240
|
*/
|
|
1152
|
-
const _removeAttribute = function _removeAttribute(name, element) {
|
|
1241
|
+
const _removeAttribute = function _removeAttribute(name, element, attr) {
|
|
1242
|
+
if (!attr) {
|
|
1243
|
+
try {
|
|
1244
|
+
attr = element.getAttributeNode(name);
|
|
1245
|
+
} catch (_) {
|
|
1246
|
+
attr = null;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
arrayPush(DOMPurify.removed, {
|
|
1250
|
+
attribute: attr || null,
|
|
1251
|
+
from: element
|
|
1252
|
+
});
|
|
1153
1253
|
try {
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1254
|
+
if (attr) {
|
|
1255
|
+
removeAttributeNode(element, attr);
|
|
1256
|
+
} else {
|
|
1257
|
+
element.removeAttribute(name);
|
|
1258
|
+
}
|
|
1158
1259
|
} catch (_) {
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1260
|
+
/* Clobbered or already-detached node - best-effort fall back to a
|
|
1261
|
+
name-based removal so the "is" handling below still runs. Note this
|
|
1262
|
+
fallback ASCII-lowercases its lookup key in an HTML document and so
|
|
1263
|
+
cannot reach a case-preserved attribute name; the cached
|
|
1264
|
+
removeAttributeNode() above is what removes those, and a clobbered
|
|
1265
|
+
form is already removed wholesale by _isClobbered() before reaching
|
|
1266
|
+
here. */
|
|
1267
|
+
try {
|
|
1268
|
+
element.removeAttribute(name);
|
|
1269
|
+
} catch (_) {}
|
|
1163
1270
|
}
|
|
1164
|
-
element.removeAttribute(name);
|
|
1165
1271
|
// We void attribute values for unremovable "is" attributes
|
|
1166
1272
|
if (name === 'is') {
|
|
1167
1273
|
if (RETURN_DOM || RETURN_DOM_FRAGMENT) {
|
|
@@ -1196,11 +1302,7 @@ function createDOMPurify() {
|
|
|
1196
1302
|
if (typeof name !== 'string' || ALLOWED_ATTR[transformCaseFunc(name)]) {
|
|
1197
1303
|
continue;
|
|
1198
1304
|
}
|
|
1199
|
-
|
|
1200
|
-
element.removeAttribute(name);
|
|
1201
|
-
} catch (_) {
|
|
1202
|
-
/* Clobbered removeAttribute on a doomed node — ignore */
|
|
1203
|
-
}
|
|
1305
|
+
_stripAttributeNode(element, attribute, name);
|
|
1204
1306
|
}
|
|
1205
1307
|
};
|
|
1206
1308
|
/**
|
|
@@ -1229,7 +1331,7 @@ function createDOMPurify() {
|
|
|
1229
1331
|
const stack = [root];
|
|
1230
1332
|
while (stack.length > 0) {
|
|
1231
1333
|
const node = stack.pop();
|
|
1232
|
-
const nodeType =
|
|
1334
|
+
const nodeType = _readNodeType(node);
|
|
1233
1335
|
if (nodeType === NODE_TYPE.element) {
|
|
1234
1336
|
_stripDisallowedAttributes(node);
|
|
1235
1337
|
}
|
|
@@ -1276,6 +1378,26 @@ function createDOMPurify() {
|
|
|
1276
1378
|
*
|
|
1277
1379
|
* @param root the in-place root to sweep
|
|
1278
1380
|
*/
|
|
1381
|
+
/**
|
|
1382
|
+
* Central policy for declarative-partial-updates patch-linkage attributes,
|
|
1383
|
+
* shared by the _neutralizePatchLinkage pre-pass and _isValidAttribute so
|
|
1384
|
+
* the two sites cannot drift: `patchsrc` always links, `for` links
|
|
1385
|
+
* everywhere except on <label>/<output>, and the whole policy is gated on
|
|
1386
|
+
* SAFE_FOR_XML (see the rationale block in _isValidAttribute).
|
|
1387
|
+
*
|
|
1388
|
+
* @param lcName the transformCaseFunc'd attribute name
|
|
1389
|
+
* @param lcTag the transformCaseFunc'd tag name of the carrying element
|
|
1390
|
+
* @return true if the attribute is patch linkage and must be dropped
|
|
1391
|
+
*/
|
|
1392
|
+
const _isPatchLinkageAttribute = function _isPatchLinkageAttribute(lcName, lcTag) {
|
|
1393
|
+
if (!SAFE_FOR_XML) {
|
|
1394
|
+
return false;
|
|
1395
|
+
}
|
|
1396
|
+
if (lcName === 'patchsrc') {
|
|
1397
|
+
return true;
|
|
1398
|
+
}
|
|
1399
|
+
return lcName === 'for' && lcTag !== 'label' && lcTag !== 'output';
|
|
1400
|
+
};
|
|
1279
1401
|
const _neutralizePatchLinkage = function _neutralizePatchLinkage(root) {
|
|
1280
1402
|
if (!SAFE_FOR_XML) {
|
|
1281
1403
|
return;
|
|
@@ -1283,7 +1405,7 @@ function createDOMPurify() {
|
|
|
1283
1405
|
const stack = [root];
|
|
1284
1406
|
while (stack.length > 0) {
|
|
1285
1407
|
const node = stack.pop();
|
|
1286
|
-
const nodeType =
|
|
1408
|
+
const nodeType = _readNodeType(node);
|
|
1287
1409
|
/* Remove range markers (the target side of a patch linkage): every
|
|
1288
1410
|
processing instruction, and any markup-bearing comment. */
|
|
1289
1411
|
if (nodeType === NODE_TYPE.processingInstruction || nodeType === NODE_TYPE.comment && regExpTest(COMMENT_MARKUP_PROBE, node.data)) {
|
|
@@ -1297,12 +1419,12 @@ function createDOMPurify() {
|
|
|
1297
1419
|
/* Strip patch-source attributes (the source side) off elements. */
|
|
1298
1420
|
if (nodeType === NODE_TYPE.element) {
|
|
1299
1421
|
const element = node;
|
|
1300
|
-
const lcTag = transformCaseFunc(
|
|
1422
|
+
const lcTag = transformCaseFunc(_readNodeName(node));
|
|
1301
1423
|
try {
|
|
1302
1424
|
if (element.hasAttribute && element.hasAttribute('patchsrc')) {
|
|
1303
1425
|
element.removeAttribute('patchsrc');
|
|
1304
1426
|
}
|
|
1305
|
-
if (element.hasAttribute && element.hasAttribute('for') &&
|
|
1427
|
+
if (element.hasAttribute && element.hasAttribute('for') && _isPatchLinkageAttribute('for', lcTag)) {
|
|
1306
1428
|
element.removeAttribute('for');
|
|
1307
1429
|
}
|
|
1308
1430
|
} catch (_) {
|
|
@@ -1477,7 +1599,17 @@ function createDOMPurify() {
|
|
|
1477
1599
|
// makes the direct read diverge from the cached read; a clean form
|
|
1478
1600
|
// (same-realm OR foreign-realm) has both reads pointing at the same
|
|
1479
1601
|
// canonical NamedNodeMap.
|
|
1480
|
-
element.attributes !== getAttributes(element) || typeof element.removeAttribute !== 'function' ||
|
|
1602
|
+
element.attributes !== getAttributes(element) || typeof element.removeAttribute !== 'function' ||
|
|
1603
|
+
// A form descendant named "removeAttributeNode" or "getAttributeNode"
|
|
1604
|
+
// shadows these Attr-node methods via [LegacyOverrideBuiltIns].
|
|
1605
|
+
// _removeAttribute() / _stripAttributeNode() reach for
|
|
1606
|
+
// element.removeAttributeNode(attr) first; when it is shadowed the call
|
|
1607
|
+
// throws and the name-based fallback element.removeAttribute(name)
|
|
1608
|
+
// ASCII-lowercases its lookup key in an HTML document, silently missing
|
|
1609
|
+
// a case-preserved event-handler attribute (e.g. an ONANIMATIONSTART
|
|
1610
|
+
// that reached the sanitizer through an XML/XHTML parse). Flag the form
|
|
1611
|
+
// so it is removed wholesale, exactly as for the other shadowed methods.
|
|
1612
|
+
typeof element.removeAttributeNode !== 'function' || typeof element.getAttributeNode !== 'function' || typeof element.setAttribute !== 'function' || typeof element.namespaceURI !== 'string' || typeof element.insertBefore !== 'function' || typeof element.hasChildNodes !== 'function' ||
|
|
1481
1613
|
// NodeType clobbering probe. Cached Node.prototype.nodeType getter
|
|
1482
1614
|
// returns the integer 1 for any Element regardless of realm; direct
|
|
1483
1615
|
// read on a clobbered form (e.g. <input name="nodeType">) returns
|
|
@@ -1564,8 +1696,16 @@ function createDOMPurify() {
|
|
|
1564
1696
|
if (SAFE_FOR_XML && currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.textContent) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.innerHTML)) {
|
|
1565
1697
|
return true;
|
|
1566
1698
|
}
|
|
1567
|
-
/* Remove
|
|
1568
|
-
|
|
1699
|
+
/* Remove rawtext/literal-text elements whose literal serialization
|
|
1700
|
+
re-opens markup on an HTML reparse - shapes (a) and (b) documented at
|
|
1701
|
+
LITERAL_TEXT_ELEMENTS. Both are invisible to rule 1 above (which
|
|
1702
|
+
self-disables once there is an element child, and whose second probe
|
|
1703
|
+
reads the innerHTML an XML/XHTML document serializes escaped), which
|
|
1704
|
+
is why both probes here read textContent instead. Previously only
|
|
1705
|
+
`style`-with-element-child was covered; every element in
|
|
1706
|
+
LITERAL_TEXT_ELEMENTS shares this literal serialization and is
|
|
1707
|
+
equally affected. */
|
|
1708
|
+
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))) {
|
|
1569
1709
|
return true;
|
|
1570
1710
|
}
|
|
1571
1711
|
/* Remove any occurrence of processing instructions */
|
|
@@ -1578,6 +1718,30 @@ function createDOMPurify() {
|
|
|
1578
1718
|
}
|
|
1579
1719
|
return false;
|
|
1580
1720
|
};
|
|
1721
|
+
/**
|
|
1722
|
+
* Evaluate a CUSTOM_ELEMENT_HANDLING check (a RegExp or a predicate
|
|
1723
|
+
* function, per the validation in _parseConfig) against a name.
|
|
1724
|
+
* Additional arguments are forwarded to predicate functions - the
|
|
1725
|
+
* attributeNameCheck predicate receives the tag name as its second
|
|
1726
|
+
* argument. A null/absent check never matches.
|
|
1727
|
+
*
|
|
1728
|
+
* @param check the configured tagNameCheck / attributeNameCheck value
|
|
1729
|
+
* @param name the name to test
|
|
1730
|
+
* @param args extra arguments forwarded to a predicate function
|
|
1731
|
+
* @return true if the check matches the name
|
|
1732
|
+
*/
|
|
1733
|
+
const _matchesNameCheck = function _matchesNameCheck(check, name) {
|
|
1734
|
+
if (check instanceof RegExp) {
|
|
1735
|
+
return regExpTest(check, name);
|
|
1736
|
+
}
|
|
1737
|
+
if (check instanceof Function) {
|
|
1738
|
+
for (var _len = arguments.length, args = new Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) {
|
|
1739
|
+
args[_key - 2] = arguments[_key];
|
|
1740
|
+
}
|
|
1741
|
+
return Boolean(check(name, ...args));
|
|
1742
|
+
}
|
|
1743
|
+
return false;
|
|
1744
|
+
};
|
|
1581
1745
|
/**
|
|
1582
1746
|
* Handle a node whose tag is forbidden or not allowlisted: keep
|
|
1583
1747
|
* allowed custom elements (false return exits _sanitizeElements
|
|
@@ -1597,13 +1761,8 @@ function createDOMPurify() {
|
|
|
1597
1761
|
*/
|
|
1598
1762
|
const _sanitizeDisallowedNode = function _sanitizeDisallowedNode(currentNode, tagName, root) {
|
|
1599
1763
|
/* Check if we have a custom element to handle */
|
|
1600
|
-
if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {
|
|
1601
|
-
|
|
1602
|
-
return false;
|
|
1603
|
-
}
|
|
1604
|
-
if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)) {
|
|
1605
|
-
return false;
|
|
1606
|
-
}
|
|
1764
|
+
if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName) && _matchesNameCheck(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) {
|
|
1765
|
+
return false;
|
|
1607
1766
|
}
|
|
1608
1767
|
/* Keep content except for bad-listed elements.
|
|
1609
1768
|
Use the cached prototype getters exclusively — the previous code
|
|
@@ -1665,6 +1824,46 @@ function createDOMPurify() {
|
|
|
1665
1824
|
}
|
|
1666
1825
|
return set === defaultSet || set === setConfigSet ? clone(set) : set;
|
|
1667
1826
|
};
|
|
1827
|
+
/**
|
|
1828
|
+
* Shared guard for a node that a hook has detached from the walk tree,
|
|
1829
|
+
* used after each element-hook site in _sanitizeElements. Detaching is a
|
|
1830
|
+
* long-standing user pattern (issue #469; draw.io-style foreignObject
|
|
1831
|
+
* filtering). Per the cached, unclobberable parentNode getter the node is
|
|
1832
|
+
* genuinely out of the tree, so it can reach neither the serialized
|
|
1833
|
+
* output nor an IN_PLACE live tree; treat it as removed and stop
|
|
1834
|
+
* processing it. Without this guard, the unsafe-node / namespace checks
|
|
1835
|
+
* would call _forceRemove on a parentless node and hit the REPORT-3
|
|
1836
|
+
* fail-closed throw — which exists for nodes DOMPurify wants gone but
|
|
1837
|
+
* *cannot* detach (clobbered / parentless roots), the opposite of a node
|
|
1838
|
+
* that is already safely gone. The walk root is exempt: a detached
|
|
1839
|
+
* IN_PLACE root is legitimate input and must still be fully sanitized,
|
|
1840
|
+
* and a kill-decision on it must keep hitting the REPORT-3 throw.
|
|
1841
|
+
*
|
|
1842
|
+
* Nodes detached by hooks stay the hook's responsibility for placement:
|
|
1843
|
+
* they are not recorded in DOMPurify.removed, so the post-walk IN_PLACE
|
|
1844
|
+
* pass (which iterates DOMPurify.removed) does not reach them. But a
|
|
1845
|
+
* hook-detached subtree can still hold a queued resource-event handler -
|
|
1846
|
+
* e.g. an <img onload> that began loading when the caller built the live
|
|
1847
|
+
* tree - which fires in page scope after sanitize returns even though the
|
|
1848
|
+
* handler never reached the returned tree. That is the audit-5 F1 hazard,
|
|
1849
|
+
* and the documented node.remove() hook pattern walks straight into it.
|
|
1850
|
+
* So on the IN_PLACE path we neutralize the detached subtree inline,
|
|
1851
|
+
* stripping its non-allow-listed attributes before returning, exactly as
|
|
1852
|
+
* the post-walk pass does for _forceRemove'd subtrees.
|
|
1853
|
+
*
|
|
1854
|
+
* @param currentNode the node a hook may have detached
|
|
1855
|
+
* @param root the current walk root
|
|
1856
|
+
* @return true if the node is detached and now handled, false otherwise
|
|
1857
|
+
*/
|
|
1858
|
+
const _handleHookDetachedNode = function _handleHookDetachedNode(currentNode, root) {
|
|
1859
|
+
if (currentNode === root || getParentNode(currentNode) !== null) {
|
|
1860
|
+
return false;
|
|
1861
|
+
}
|
|
1862
|
+
if (IN_PLACE) {
|
|
1863
|
+
_neutralizeSubtree(currentNode);
|
|
1864
|
+
}
|
|
1865
|
+
return true;
|
|
1866
|
+
};
|
|
1668
1867
|
/**
|
|
1669
1868
|
* _sanitizeElements
|
|
1670
1869
|
*
|
|
@@ -1674,18 +1873,12 @@ function createDOMPurify() {
|
|
|
1674
1873
|
* @param currentNode to check for permission to exist
|
|
1675
1874
|
* @return true if node was killed, false if left alive
|
|
1676
1875
|
*/
|
|
1677
|
-
// eslint-disable-next-line complexity
|
|
1678
1876
|
const _sanitizeElements = function _sanitizeElements(currentNode, root) {
|
|
1679
1877
|
/* Execute a hook if present */
|
|
1680
1878
|
_executeHooks(hooks.beforeSanitizeElements, currentNode, null);
|
|
1681
|
-
/* A hook may have detached the node - treat it as removed (see
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
resource handler on it cannot fire in page scope after we return. */
|
|
1685
|
-
if (currentNode !== root && getParentNode(currentNode) === null) {
|
|
1686
|
-
if (IN_PLACE) {
|
|
1687
|
-
_neutralizeSubtree(currentNode);
|
|
1688
|
-
}
|
|
1879
|
+
/* A hook may have detached the node - treat it as removed (see
|
|
1880
|
+
_handleHookDetachedNode for the full rationale). */
|
|
1881
|
+
if (_handleHookDetachedNode(currentNode, root)) {
|
|
1689
1882
|
return true;
|
|
1690
1883
|
}
|
|
1691
1884
|
/* Check if element is clobbered or can clobber */
|
|
@@ -1694,7 +1887,7 @@ function createDOMPurify() {
|
|
|
1694
1887
|
return true;
|
|
1695
1888
|
}
|
|
1696
1889
|
/* Now let's check the element's type and name */
|
|
1697
|
-
const tagName = transformCaseFunc(
|
|
1890
|
+
const tagName = transformCaseFunc(_readNodeName(currentNode));
|
|
1698
1891
|
/* Close the pre-walk clone-guard's timing gap: an uponSanitizeElement
|
|
1699
1892
|
hook may have been installed after that guard sampled the hook arrays
|
|
1700
1893
|
(e.g. lazily from beforeSanitizeElements), leaving ALLOWED_TAGS still
|
|
@@ -1706,34 +1899,9 @@ function createDOMPurify() {
|
|
|
1706
1899
|
tagName,
|
|
1707
1900
|
allowedTags: ALLOWED_TAGS
|
|
1708
1901
|
});
|
|
1709
|
-
/*
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
genuinely out of the tree, so it can reach neither the serialized
|
|
1713
|
-
output nor an IN_PLACE live tree; treat it as removed and stop
|
|
1714
|
-
processing it. Without this guard, the unsafe-node / namespace
|
|
1715
|
-
checks below would call _forceRemove on a parentless node and hit
|
|
1716
|
-
the REPORT-3 fail-closed throw — which exists for nodes DOMPurify
|
|
1717
|
-
wants gone but *cannot* detach (clobbered / parentless roots), the
|
|
1718
|
-
opposite of a node that is already safely gone. The walk root is
|
|
1719
|
-
exempt: a detached IN_PLACE root is legitimate input and must still
|
|
1720
|
-
be fully sanitized, and a kill-decision on it must keep hitting the
|
|
1721
|
-
REPORT-3 throw. Nodes detached by hooks stay the hook's
|
|
1722
|
-
responsibility for placement: they are not recorded in
|
|
1723
|
-
DOMPurify.removed, so the post-walk IN_PLACE pass (which iterates
|
|
1724
|
-
DOMPurify.removed) does not reach them. But a hook-detached subtree
|
|
1725
|
-
can still hold a queued resource-event handler - e.g. an <img onload>
|
|
1726
|
-
that began loading when the caller built the live tree - which fires
|
|
1727
|
-
in page scope after sanitize returns even though the handler never
|
|
1728
|
-
reached the returned tree. That is the audit-5 F1 hazard, and the
|
|
1729
|
-
documented node.remove() hook pattern walks straight into it. So on
|
|
1730
|
-
the IN_PLACE path we neutralize the detached subtree inline here,
|
|
1731
|
-
stripping its non-allow-listed attributes before returning, exactly
|
|
1732
|
-
as the post-walk pass does for _forceRemove'd subtrees. */
|
|
1733
|
-
if (currentNode !== root && getParentNode(currentNode) === null) {
|
|
1734
|
-
if (IN_PLACE) {
|
|
1735
|
-
_neutralizeSubtree(currentNode);
|
|
1736
|
-
}
|
|
1902
|
+
/* The uponSanitizeElement hook may have detached the node, exactly as
|
|
1903
|
+
above (see _handleHookDetachedNode for the full rationale). */
|
|
1904
|
+
if (_handleHookDetachedNode(currentNode, root)) {
|
|
1737
1905
|
return true;
|
|
1738
1906
|
}
|
|
1739
1907
|
/* Remove mXSS vectors, processing instructions and risky comments */
|
|
@@ -1766,7 +1934,7 @@ function createDOMPurify() {
|
|
|
1766
1934
|
bound and short-circuits to false for any node minted in a different
|
|
1767
1935
|
realm — letting a foreign-realm element with a forbidden namespace
|
|
1768
1936
|
slip past the namespace check entirely. */
|
|
1769
|
-
const nt =
|
|
1937
|
+
const nt = _readNodeType(currentNode);
|
|
1770
1938
|
if (nt === NODE_TYPE.element && !_checkValidNamespace(currentNode)) {
|
|
1771
1939
|
_forceRemove(currentNode);
|
|
1772
1940
|
return true;
|
|
@@ -1826,10 +1994,7 @@ function createDOMPurify() {
|
|
|
1826
1994
|
other structural-threat checks and stays overridable, consistent with
|
|
1827
1995
|
the rest of the codebase. PI range markers are already removed by
|
|
1828
1996
|
_isUnsafeNode. */
|
|
1829
|
-
if (
|
|
1830
|
-
return false;
|
|
1831
|
-
}
|
|
1832
|
-
if (SAFE_FOR_XML && lcName === 'for' && lcTag !== 'label' && lcTag !== 'output') {
|
|
1997
|
+
if (_isPatchLinkageAttribute(lcName, lcTag)) {
|
|
1833
1998
|
return false;
|
|
1834
1999
|
}
|
|
1835
2000
|
/* Make sure attribute cannot clobber */
|
|
@@ -1841,22 +2006,54 @@ function createDOMPurify() {
|
|
|
1841
2006
|
(https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes)
|
|
1842
2007
|
XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804)
|
|
1843
2008
|
We don't need to check the value; it's always URI safe. */
|
|
1844
|
-
if (ALLOW_DATA_ATTR && regExpTest(DATA_ATTR$1, lcName))
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
2009
|
+
if (ALLOW_DATA_ATTR && regExpTest(DATA_ATTR$1, lcName)) {
|
|
2010
|
+
return true;
|
|
2011
|
+
}
|
|
2012
|
+
/* Allow valid aria-* attributes, the value is always URI safe */
|
|
2013
|
+
if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR$1, lcName)) {
|
|
2014
|
+
return true;
|
|
2015
|
+
}
|
|
2016
|
+
/* A name outside the allowlist is acceptable on custom-element terms
|
|
2017
|
+
only. The value checks below are intentionally skipped in that case:
|
|
2018
|
+
if the user supplied a tagNameCheck we also allow derived custom
|
|
2019
|
+
elements using the same test, and attributes passing the configured
|
|
2020
|
+
attributeNameCheck are allowed as custom elements define these at
|
|
2021
|
+
their own discretion. */
|
|
2022
|
+
if (!nameIsPermitted) {
|
|
2023
|
+
return (
|
|
2024
|
+
// Condition a) covers a basically valid custom element tag name whose
|
|
2025
|
+
// tag passes the configured tagNameCheck and whose attribute name
|
|
2026
|
+
// passes the configured attributeNameCheck ...
|
|
2027
|
+
_isBasicCustomElement(lcTag) && _matchesNameCheck(CUSTOM_ELEMENT_HANDLING.tagNameCheck, lcTag) && _matchesNameCheck(CUSTOM_ELEMENT_HANDLING.attributeNameCheck, lcName, lcTag) ||
|
|
2028
|
+
// Condition b) covers an `is` attribute whose value passes the
|
|
2029
|
+
// configured tagNameCheck while customized built-in elements are
|
|
2030
|
+
// allowed.
|
|
2031
|
+
lcName === 'is' && CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements && _matchesNameCheck(CUSTOM_ELEMENT_HANDLING.tagNameCheck, value)
|
|
2032
|
+
);
|
|
2033
|
+
}
|
|
2034
|
+
/* Check value is safe. First, is attr inert? If so, is safe */
|
|
2035
|
+
if (URI_SAFE_ATTRIBUTES[lcName]) {
|
|
2036
|
+
return true;
|
|
2037
|
+
}
|
|
2038
|
+
/* Check no script, data or unknown possibly unsafe URI
|
|
2039
|
+
unless we know URI values are safe for that attribute */
|
|
2040
|
+
if (regExpTest(IS_ALLOWED_URI$1, stringReplace(value, ATTR_WHITESPACE$1, ''))) {
|
|
2041
|
+
return true;
|
|
2042
|
+
}
|
|
2043
|
+
/* Keep image data URIs alive if src/xlink:href is allowed */
|
|
2044
|
+
/* Further prevent gadget XSS for dynamically built script tags */
|
|
2045
|
+
if ((lcName === 'src' || lcName === 'xlink:href' || lcName === 'href') && lcTag !== 'script' && stringIndexOf(value, 'data:') === 0 && DATA_URI_TAGS[lcTag]) {
|
|
2046
|
+
return true;
|
|
2047
|
+
}
|
|
2048
|
+
/* Allow unknown protocols: This provides support for links that
|
|
2049
|
+
are handled by protocol handlers which may be unknown ahead of
|
|
2050
|
+
time, e.g. fb:, spotify: */
|
|
2051
|
+
if (ALLOW_UNKNOWN_PROTOCOLS && !regExpTest(IS_SCRIPT_OR_DATA$1, stringReplace(value, ATTR_WHITESPACE$1, ''))) {
|
|
2052
|
+
return true;
|
|
2053
|
+
}
|
|
2054
|
+
/* Only an empty (binary) value remains safe at this point;
|
|
2055
|
+
anything else is presumed unsafe, do not add it back */
|
|
2056
|
+
return !value;
|
|
1860
2057
|
};
|
|
1861
2058
|
/* Names the HTML spec reserves from valid-custom-element-name; these must
|
|
1862
2059
|
* never be treated as basic custom elements even when a permissive
|
|
@@ -1903,15 +2100,25 @@ function createDOMPurify() {
|
|
|
1903
2100
|
/**
|
|
1904
2101
|
* Write a modified attribute value back onto the element. On
|
|
1905
2102
|
* success, re-probe for clobbering introduced by the new value and
|
|
1906
|
-
* remove the element when found; otherwise
|
|
1907
|
-
*
|
|
1908
|
-
*
|
|
2103
|
+
* remove the element when found; otherwise, when this writeback is the
|
|
2104
|
+
* recreate half of the SANITIZE_NAMED_PROPS remove-and-recreate, pop the
|
|
2105
|
+
* removal entry that path recorded so it does not show as removed. On
|
|
1909
2106
|
* failure, remove the attribute instead.
|
|
1910
2107
|
*
|
|
2108
|
+
* Returns true only on a clean write (the value was set and the new value
|
|
2109
|
+
* introduced no clobbering). The caller uses that, together with its own
|
|
2110
|
+
* knowledge of whether this attribute pushed a DOMPurify.removed record, to
|
|
2111
|
+
* decide whether to pop that record. The pop must happen ONLY for the
|
|
2112
|
+
* named-prop remove-and-recreate; popping on any other value change (trim,
|
|
2113
|
+
* template scrubbing, Trusted Types) would consume an unrelated _forceRemove
|
|
2114
|
+
* subtree-cleanup record and let that detached subtree keep a live event
|
|
2115
|
+
* handler through the IN_PLACE neutralization pass (SO-001).
|
|
2116
|
+
*
|
|
1911
2117
|
* @param currentNode the element carrying the attribute
|
|
1912
2118
|
* @param name the attribute name as present on the element
|
|
1913
2119
|
* @param namespaceURI the attribute's namespace, if any
|
|
1914
2120
|
* @param value the new attribute value
|
|
2121
|
+
* @return true if the value was written without introducing clobbering
|
|
1915
2122
|
*/
|
|
1916
2123
|
const _setAttributeValue = function _setAttributeValue(currentNode, name, namespaceURI, value) {
|
|
1917
2124
|
try {
|
|
@@ -1923,11 +2130,12 @@ function createDOMPurify() {
|
|
|
1923
2130
|
}
|
|
1924
2131
|
if (_isClobbered(currentNode)) {
|
|
1925
2132
|
_forceRemove(currentNode);
|
|
1926
|
-
|
|
1927
|
-
arrayPop(DOMPurify.removed);
|
|
2133
|
+
return false;
|
|
1928
2134
|
}
|
|
2135
|
+
return true;
|
|
1929
2136
|
} catch (_) {
|
|
1930
2137
|
_removeAttribute(name, currentNode);
|
|
2138
|
+
return false;
|
|
1931
2139
|
}
|
|
1932
2140
|
};
|
|
1933
2141
|
/**
|
|
@@ -1940,6 +2148,7 @@ function createDOMPurify() {
|
|
|
1940
2148
|
*
|
|
1941
2149
|
* @param currentNode to sanitize
|
|
1942
2150
|
*/
|
|
2151
|
+
// eslint-disable-next-line complexity
|
|
1943
2152
|
const _sanitizeAttributes = function _sanitizeAttributes(currentNode) {
|
|
1944
2153
|
/* Execute a hook if present */
|
|
1945
2154
|
_executeHooks(hooks.beforeSanitizeAttributes, currentNode, null);
|
|
@@ -1969,6 +2178,15 @@ function createDOMPurify() {
|
|
|
1969
2178
|
const lcName = transformCaseFunc(name);
|
|
1970
2179
|
const initValue = attrValue;
|
|
1971
2180
|
let value = name === 'value' ? initValue : stringTrim(initValue);
|
|
2181
|
+
/* Tracks whether this specific attribute pushed a DOMPurify.removed
|
|
2182
|
+
record that a later _setAttributeValue() writeback is expected to pop
|
|
2183
|
+
back off (the SANITIZE_NAMED_PROPS remove-and-recreate below is the
|
|
2184
|
+
only path that does). A value that merely changed via trim, template
|
|
2185
|
+
scrubbing, or Trusted Types pushes no such record, so its writeback
|
|
2186
|
+
must NOT pop — otherwise it consumes an unrelated _forceRemove subtree
|
|
2187
|
+
record and that detached subtree escapes the IN_PLACE handler-
|
|
2188
|
+
neutralization pass (SO-001). */
|
|
2189
|
+
let recreatedNamedProp = false;
|
|
1972
2190
|
/* Execute a hook if present */
|
|
1973
2191
|
hookEvent.attrName = lcName;
|
|
1974
2192
|
hookEvent.attrValue = value;
|
|
@@ -1981,20 +2199,23 @@ function createDOMPurify() {
|
|
|
1981
2199
|
*/
|
|
1982
2200
|
if (SANITIZE_NAMED_PROPS && (lcName === 'id' || lcName === 'name') && stringIndexOf(value, SANITIZE_NAMED_PROPS_PREFIX) !== 0) {
|
|
1983
2201
|
// Remove the attribute with this value
|
|
1984
|
-
_removeAttribute(name, currentNode);
|
|
2202
|
+
_removeAttribute(name, currentNode, attr);
|
|
1985
2203
|
// Prefix the value and later re-create the attribute with the sanitized value
|
|
1986
2204
|
value = SANITIZE_NAMED_PROPS_PREFIX + value;
|
|
2205
|
+
// This attribute owns the record just pushed; its recreate writeback
|
|
2206
|
+
// is the one allowed to pop it back off.
|
|
2207
|
+
recreatedNamedProp = true;
|
|
1987
2208
|
}
|
|
1988
2209
|
// Else: already prefixed, leave the attribute alone — the prefix is
|
|
1989
2210
|
// itself the clobbering protection, and re-applying it is incorrect.
|
|
1990
2211
|
/* Work around a security issue with comments inside attributes */
|
|
1991
2212
|
if (SAFE_FOR_XML && regExpTest(/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i, value)) {
|
|
1992
|
-
_removeAttribute(name, currentNode);
|
|
2213
|
+
_removeAttribute(name, currentNode, attr);
|
|
1993
2214
|
continue;
|
|
1994
2215
|
}
|
|
1995
2216
|
/* Make sure we cannot easily use animated hrefs, even if animations are allowed */
|
|
1996
2217
|
if (lcName === 'attributename' && stringMatch(value, 'href')) {
|
|
1997
|
-
_removeAttribute(name, currentNode);
|
|
2218
|
+
_removeAttribute(name, currentNode, attr);
|
|
1998
2219
|
continue;
|
|
1999
2220
|
}
|
|
2000
2221
|
/* Did the hooks force-keep the attribute? */
|
|
@@ -2003,12 +2224,12 @@ function createDOMPurify() {
|
|
|
2003
2224
|
}
|
|
2004
2225
|
/* Did the hooks approve of the attribute? */
|
|
2005
2226
|
if (!hookEvent.keepAttr) {
|
|
2006
|
-
_removeAttribute(name, currentNode);
|
|
2227
|
+
_removeAttribute(name, currentNode, attr);
|
|
2007
2228
|
continue;
|
|
2008
2229
|
}
|
|
2009
2230
|
/* Work around a security issue in jQuery 3.0 */
|
|
2010
2231
|
if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(SELF_CLOSING_TAG, value)) {
|
|
2011
|
-
_removeAttribute(name, currentNode);
|
|
2232
|
+
_removeAttribute(name, currentNode, attr);
|
|
2012
2233
|
continue;
|
|
2013
2234
|
}
|
|
2014
2235
|
/* Sanitize attribute content to be template-safe */
|
|
@@ -2017,14 +2238,21 @@ function createDOMPurify() {
|
|
|
2017
2238
|
}
|
|
2018
2239
|
/* Is `value` valid for this attribute? */
|
|
2019
2240
|
if (!_isValidAttribute(lcTag, lcName, value)) {
|
|
2020
|
-
_removeAttribute(name, currentNode);
|
|
2241
|
+
_removeAttribute(name, currentNode, attr);
|
|
2021
2242
|
continue;
|
|
2022
2243
|
}
|
|
2023
2244
|
/* Handle attributes that require Trusted Types */
|
|
2024
2245
|
value = _applyTrustedTypesToAttribute(lcTag, lcName, namespaceURI, value);
|
|
2025
2246
|
/* Handle invalid data-* attribute set by try-catching it */
|
|
2026
2247
|
if (value !== initValue) {
|
|
2027
|
-
_setAttributeValue(currentNode, name, namespaceURI, value);
|
|
2248
|
+
const cleanWrite = _setAttributeValue(currentNode, name, namespaceURI, value);
|
|
2249
|
+
/* Only the named-prop remove-and-recreate owns a DOMPurify.removed
|
|
2250
|
+
record to pop; a plain trim/template/Trusted-Types writeback owns
|
|
2251
|
+
none, so popping here would drop an unrelated subtree's cleanup
|
|
2252
|
+
record (SO-001). */
|
|
2253
|
+
if (cleanWrite && recreatedNamedProp) {
|
|
2254
|
+
arrayPop(DOMPurify.removed);
|
|
2255
|
+
}
|
|
2028
2256
|
}
|
|
2029
2257
|
}
|
|
2030
2258
|
/* Execute a hook if present */
|
|
@@ -2064,8 +2292,7 @@ function createDOMPurify() {
|
|
|
2064
2292
|
Walk it explicitly. The nodeType guard avoids reading
|
|
2065
2293
|
shadowRoot off text / comment / CDATA / PI nodes that the
|
|
2066
2294
|
iterator also surfaces. */
|
|
2067
|
-
|
|
2068
|
-
if (shadowNodeType === NODE_TYPE.element) {
|
|
2295
|
+
if (_readNodeType(shadowNode) === NODE_TYPE.element) {
|
|
2069
2296
|
const innerSr = getShadowRoot(shadowNode);
|
|
2070
2297
|
if (_isDocumentFragment(innerSr)) {
|
|
2071
2298
|
_sanitizeAttachedShadowRoots(innerSr);
|
|
@@ -2123,7 +2350,7 @@ function createDOMPurify() {
|
|
|
2123
2350
|
continue;
|
|
2124
2351
|
}
|
|
2125
2352
|
const node = item.node;
|
|
2126
|
-
const nodeType =
|
|
2353
|
+
const nodeType = _readNodeType(node);
|
|
2127
2354
|
const isElement = nodeType === NODE_TYPE.element;
|
|
2128
2355
|
/* (pushed last → processed first) Children, snapshotted in reverse so
|
|
2129
2356
|
the first child is processed first. Snapshotting matters because a
|
|
@@ -2245,7 +2472,7 @@ function createDOMPurify() {
|
|
|
2245
2472
|
child named "nodeName" on the form root would otherwise shadow
|
|
2246
2473
|
the property and let this check skip the root-allowlist
|
|
2247
2474
|
validation entirely. */
|
|
2248
|
-
const nn =
|
|
2475
|
+
const nn = _readNodeName(dirty);
|
|
2249
2476
|
if (typeof nn === 'string') {
|
|
2250
2477
|
const tagName = transformCaseFunc(nn);
|
|
2251
2478
|
if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {
|
|
@@ -2301,8 +2528,15 @@ function createDOMPurify() {
|
|
|
2301
2528
|
them before the main iterator runs, since the iterator does not
|
|
2302
2529
|
descend into shadow trees. The walk routes every read through a
|
|
2303
2530
|
cached prototype getter so clobbering descendants on a form root
|
|
2304
|
-
cannot hide a shadow host from this pass.
|
|
2305
|
-
|
|
2531
|
+
cannot hide a shadow host from this pass.
|
|
2532
|
+
Traverse from `body`, not `importedNode`: when importedNode is a
|
|
2533
|
+
DocumentFragment, the appendChild() above moves its children into
|
|
2534
|
+
`body` and leaves the fragment empty, so walking importedNode would
|
|
2535
|
+
scan nothing and miss every attached shadow host now under `body`
|
|
2536
|
+
(SO-003). In the BODY/HTML branches `body === importedNode`, so this
|
|
2537
|
+
is equivalent there; in the element branch `body` contains the
|
|
2538
|
+
appended element and its descendants. `body` covers all three. */
|
|
2539
|
+
_sanitizeAttachedShadowRoots(body);
|
|
2306
2540
|
} else {
|
|
2307
2541
|
/* Exit directly if we have nothing to do */
|
|
2308
2542
|
if (!RETURN_DOM && !SAFE_FOR_TEMPLATES && !WHOLE_DOCUMENT &&
|