dompurify 3.4.13 → 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.
- package/README.md +10 -2
- package/dist/purify.cjs.d.ts +1 -1
- package/dist/purify.cjs.js +292 -117
- package/dist/purify.cjs.js.map +1 -1
- package/dist/purify.es.d.mts +1 -1
- package/dist/purify.es.mjs +292 -117
- package/dist/purify.es.mjs.map +1 -1
- package/dist/purify.js +292 -117
- 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 +361 -201
- package/src/types.ts +2 -5
package/dist/purify.cjs.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! @license DOMPurify 3.4.
|
|
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.
|
|
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
|
|
@@ -458,6 +504,16 @@ function createDOMPurify() {
|
|
|
458
504
|
const getNodeType = Node && Node.prototype ? lookupGetter(Node.prototype, 'nodeType') : null;
|
|
459
505
|
const getNodeName = Node && Node.prototype ? lookupGetter(Node.prototype, 'nodeName') : null;
|
|
460
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
|
+
};
|
|
461
517
|
// As per issue #47, the web-components registry is inherited by a
|
|
462
518
|
// new document created via createHTMLDocument. As per the spec
|
|
463
519
|
// (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries)
|
|
@@ -780,9 +836,11 @@ function createDOMPurify() {
|
|
|
780
836
|
IN_PLACE = cfg.IN_PLACE || false; // Default false
|
|
781
837
|
IS_ALLOWED_URI$1 = isRegex(cfg.ALLOWED_URI_REGEXP) ? cfg.ALLOWED_URI_REGEXP : IS_ALLOWED_URI; // Default regexp
|
|
782
838
|
NAMESPACE = typeof cfg.NAMESPACE === 'string' ? cfg.NAMESPACE : HTML_NAMESPACE; // Default HTML namespace
|
|
783
|
-
MATHML_TEXT_INTEGRATION_POINTS =
|
|
784
|
-
|
|
785
|
-
|
|
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));
|
|
786
844
|
CUSTOM_ELEMENT_HANDLING = create(null);
|
|
787
845
|
if (objectHasOwnProperty(customElementHandling, 'tagNameCheck') && isRegexOrFunction(customElementHandling.tagNameCheck)) {
|
|
788
846
|
CUSTOM_ELEMENT_HANDLING.tagNameCheck = customElementHandling.tagNameCheck; // Default undefined
|
|
@@ -849,15 +907,6 @@ function createDOMPurify() {
|
|
|
849
907
|
addToSet(ALLOWED_ATTR, cfg.ADD_ATTR, transformCaseFunc);
|
|
850
908
|
}
|
|
851
909
|
}
|
|
852
|
-
if (objectHasOwnProperty(cfg, 'ADD_URI_SAFE_ATTR') && arrayIsArray(cfg.ADD_URI_SAFE_ATTR)) {
|
|
853
|
-
addToSet(URI_SAFE_ATTRIBUTES, cfg.ADD_URI_SAFE_ATTR, transformCaseFunc);
|
|
854
|
-
}
|
|
855
|
-
if (objectHasOwnProperty(cfg, 'FORBID_CONTENTS') && arrayIsArray(cfg.FORBID_CONTENTS)) {
|
|
856
|
-
if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) {
|
|
857
|
-
FORBID_CONTENTS = clone(FORBID_CONTENTS);
|
|
858
|
-
}
|
|
859
|
-
addToSet(FORBID_CONTENTS, cfg.FORBID_CONTENTS, transformCaseFunc);
|
|
860
|
-
}
|
|
861
910
|
if (objectHasOwnProperty(cfg, 'ADD_FORBID_CONTENTS') && arrayIsArray(cfg.ADD_FORBID_CONTENTS)) {
|
|
862
911
|
if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) {
|
|
863
912
|
FORBID_CONTENTS = clone(FORBID_CONTENTS);
|
|
@@ -1092,6 +1141,32 @@ function createDOMPurify() {
|
|
|
1092
1141
|
}
|
|
1093
1142
|
}
|
|
1094
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
|
+
};
|
|
1095
1170
|
/**
|
|
1096
1171
|
* _neutralizeRoot
|
|
1097
1172
|
*
|
|
@@ -1136,11 +1211,7 @@ function createDOMPurify() {
|
|
|
1136
1211
|
const attribute = attributes[i];
|
|
1137
1212
|
const name = attribute && attribute.name;
|
|
1138
1213
|
if (typeof name === 'string') {
|
|
1139
|
-
|
|
1140
|
-
root.removeAttribute(name);
|
|
1141
|
-
} catch (_) {
|
|
1142
|
-
/* Clobbered removeAttribute — ignore (fail-closed best effort) */
|
|
1143
|
-
}
|
|
1214
|
+
_stripAttributeNode(root, attribute, name);
|
|
1144
1215
|
}
|
|
1145
1216
|
}
|
|
1146
1217
|
}
|
|
@@ -1148,22 +1219,46 @@ function createDOMPurify() {
|
|
|
1148
1219
|
/**
|
|
1149
1220
|
* _removeAttribute
|
|
1150
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
|
+
*
|
|
1151
1233
|
* @param name an Attribute name
|
|
1152
1234
|
* @param element a DOM node
|
|
1235
|
+
* @param attr the exact Attr node to remove, when the caller has it
|
|
1153
1236
|
*/
|
|
1154
|
-
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
|
+
});
|
|
1155
1249
|
try {
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1250
|
+
if (attr) {
|
|
1251
|
+
element.removeAttributeNode(attr);
|
|
1252
|
+
} else {
|
|
1253
|
+
element.removeAttribute(name);
|
|
1254
|
+
}
|
|
1160
1255
|
} catch (_) {
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
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 (_) {}
|
|
1165
1261
|
}
|
|
1166
|
-
element.removeAttribute(name);
|
|
1167
1262
|
// We void attribute values for unremovable "is" attributes
|
|
1168
1263
|
if (name === 'is') {
|
|
1169
1264
|
if (RETURN_DOM || RETURN_DOM_FRAGMENT) {
|
|
@@ -1198,11 +1293,7 @@ function createDOMPurify() {
|
|
|
1198
1293
|
if (typeof name !== 'string' || ALLOWED_ATTR[transformCaseFunc(name)]) {
|
|
1199
1294
|
continue;
|
|
1200
1295
|
}
|
|
1201
|
-
|
|
1202
|
-
element.removeAttribute(name);
|
|
1203
|
-
} catch (_) {
|
|
1204
|
-
/* Clobbered removeAttribute on a doomed node — ignore */
|
|
1205
|
-
}
|
|
1296
|
+
_stripAttributeNode(element, attribute, name);
|
|
1206
1297
|
}
|
|
1207
1298
|
};
|
|
1208
1299
|
/**
|
|
@@ -1231,7 +1322,7 @@ function createDOMPurify() {
|
|
|
1231
1322
|
const stack = [root];
|
|
1232
1323
|
while (stack.length > 0) {
|
|
1233
1324
|
const node = stack.pop();
|
|
1234
|
-
const nodeType =
|
|
1325
|
+
const nodeType = _readNodeType(node);
|
|
1235
1326
|
if (nodeType === NODE_TYPE.element) {
|
|
1236
1327
|
_stripDisallowedAttributes(node);
|
|
1237
1328
|
}
|
|
@@ -1278,6 +1369,26 @@ function createDOMPurify() {
|
|
|
1278
1369
|
*
|
|
1279
1370
|
* @param root the in-place root to sweep
|
|
1280
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
|
+
};
|
|
1281
1392
|
const _neutralizePatchLinkage = function _neutralizePatchLinkage(root) {
|
|
1282
1393
|
if (!SAFE_FOR_XML) {
|
|
1283
1394
|
return;
|
|
@@ -1285,7 +1396,7 @@ function createDOMPurify() {
|
|
|
1285
1396
|
const stack = [root];
|
|
1286
1397
|
while (stack.length > 0) {
|
|
1287
1398
|
const node = stack.pop();
|
|
1288
|
-
const nodeType =
|
|
1399
|
+
const nodeType = _readNodeType(node);
|
|
1289
1400
|
/* Remove range markers (the target side of a patch linkage): every
|
|
1290
1401
|
processing instruction, and any markup-bearing comment. */
|
|
1291
1402
|
if (nodeType === NODE_TYPE.processingInstruction || nodeType === NODE_TYPE.comment && regExpTest(COMMENT_MARKUP_PROBE, node.data)) {
|
|
@@ -1299,12 +1410,12 @@ function createDOMPurify() {
|
|
|
1299
1410
|
/* Strip patch-source attributes (the source side) off elements. */
|
|
1300
1411
|
if (nodeType === NODE_TYPE.element) {
|
|
1301
1412
|
const element = node;
|
|
1302
|
-
const lcTag = transformCaseFunc(
|
|
1413
|
+
const lcTag = transformCaseFunc(_readNodeName(node));
|
|
1303
1414
|
try {
|
|
1304
1415
|
if (element.hasAttribute && element.hasAttribute('patchsrc')) {
|
|
1305
1416
|
element.removeAttribute('patchsrc');
|
|
1306
1417
|
}
|
|
1307
|
-
if (element.hasAttribute && element.hasAttribute('for') &&
|
|
1418
|
+
if (element.hasAttribute && element.hasAttribute('for') && _isPatchLinkageAttribute('for', lcTag)) {
|
|
1308
1419
|
element.removeAttribute('for');
|
|
1309
1420
|
}
|
|
1310
1421
|
} catch (_) {
|
|
@@ -1566,8 +1677,16 @@ function createDOMPurify() {
|
|
|
1566
1677
|
if (SAFE_FOR_XML && currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.textContent) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.innerHTML)) {
|
|
1567
1678
|
return true;
|
|
1568
1679
|
}
|
|
1569
|
-
/* Remove
|
|
1570
|
-
|
|
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))) {
|
|
1571
1690
|
return true;
|
|
1572
1691
|
}
|
|
1573
1692
|
/* Remove any occurrence of processing instructions */
|
|
@@ -1580,6 +1699,30 @@ function createDOMPurify() {
|
|
|
1580
1699
|
}
|
|
1581
1700
|
return false;
|
|
1582
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
|
+
};
|
|
1583
1726
|
/**
|
|
1584
1727
|
* Handle a node whose tag is forbidden or not allowlisted: keep
|
|
1585
1728
|
* allowed custom elements (false return exits _sanitizeElements
|
|
@@ -1599,13 +1742,8 @@ function createDOMPurify() {
|
|
|
1599
1742
|
*/
|
|
1600
1743
|
const _sanitizeDisallowedNode = function _sanitizeDisallowedNode(currentNode, tagName, root) {
|
|
1601
1744
|
/* Check if we have a custom element to handle */
|
|
1602
|
-
if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {
|
|
1603
|
-
|
|
1604
|
-
return false;
|
|
1605
|
-
}
|
|
1606
|
-
if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)) {
|
|
1607
|
-
return false;
|
|
1608
|
-
}
|
|
1745
|
+
if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName) && _matchesNameCheck(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) {
|
|
1746
|
+
return false;
|
|
1609
1747
|
}
|
|
1610
1748
|
/* Keep content except for bad-listed elements.
|
|
1611
1749
|
Use the cached prototype getters exclusively — the previous code
|
|
@@ -1667,6 +1805,46 @@ function createDOMPurify() {
|
|
|
1667
1805
|
}
|
|
1668
1806
|
return set === defaultSet || set === setConfigSet ? clone(set) : set;
|
|
1669
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
|
+
};
|
|
1670
1848
|
/**
|
|
1671
1849
|
* _sanitizeElements
|
|
1672
1850
|
*
|
|
@@ -1676,18 +1854,12 @@ function createDOMPurify() {
|
|
|
1676
1854
|
* @param currentNode to check for permission to exist
|
|
1677
1855
|
* @return true if node was killed, false if left alive
|
|
1678
1856
|
*/
|
|
1679
|
-
// eslint-disable-next-line complexity
|
|
1680
1857
|
const _sanitizeElements = function _sanitizeElements(currentNode, root) {
|
|
1681
1858
|
/* Execute a hook if present */
|
|
1682
1859
|
_executeHooks(hooks.beforeSanitizeElements, currentNode, null);
|
|
1683
|
-
/* A hook may have detached the node - treat it as removed (see
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
resource handler on it cannot fire in page scope after we return. */
|
|
1687
|
-
if (currentNode !== root && getParentNode(currentNode) === null) {
|
|
1688
|
-
if (IN_PLACE) {
|
|
1689
|
-
_neutralizeSubtree(currentNode);
|
|
1690
|
-
}
|
|
1860
|
+
/* A hook may have detached the node - treat it as removed (see
|
|
1861
|
+
_handleHookDetachedNode for the full rationale). */
|
|
1862
|
+
if (_handleHookDetachedNode(currentNode, root)) {
|
|
1691
1863
|
return true;
|
|
1692
1864
|
}
|
|
1693
1865
|
/* Check if element is clobbered or can clobber */
|
|
@@ -1696,7 +1868,7 @@ function createDOMPurify() {
|
|
|
1696
1868
|
return true;
|
|
1697
1869
|
}
|
|
1698
1870
|
/* Now let's check the element's type and name */
|
|
1699
|
-
const tagName = transformCaseFunc(
|
|
1871
|
+
const tagName = transformCaseFunc(_readNodeName(currentNode));
|
|
1700
1872
|
/* Close the pre-walk clone-guard's timing gap: an uponSanitizeElement
|
|
1701
1873
|
hook may have been installed after that guard sampled the hook arrays
|
|
1702
1874
|
(e.g. lazily from beforeSanitizeElements), leaving ALLOWED_TAGS still
|
|
@@ -1708,34 +1880,9 @@ function createDOMPurify() {
|
|
|
1708
1880
|
tagName,
|
|
1709
1881
|
allowedTags: ALLOWED_TAGS
|
|
1710
1882
|
});
|
|
1711
|
-
/*
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
genuinely out of the tree, so it can reach neither the serialized
|
|
1715
|
-
output nor an IN_PLACE live tree; treat it as removed and stop
|
|
1716
|
-
processing it. Without this guard, the unsafe-node / namespace
|
|
1717
|
-
checks below would call _forceRemove on a parentless node and hit
|
|
1718
|
-
the REPORT-3 fail-closed throw — which exists for nodes DOMPurify
|
|
1719
|
-
wants gone but *cannot* detach (clobbered / parentless roots), the
|
|
1720
|
-
opposite of a node that is already safely gone. The walk root is
|
|
1721
|
-
exempt: a detached IN_PLACE root is legitimate input and must still
|
|
1722
|
-
be fully sanitized, and a kill-decision on it must keep hitting the
|
|
1723
|
-
REPORT-3 throw. Nodes detached by hooks stay the hook's
|
|
1724
|
-
responsibility for placement: they are not recorded in
|
|
1725
|
-
DOMPurify.removed, so the post-walk IN_PLACE pass (which iterates
|
|
1726
|
-
DOMPurify.removed) does not reach them. But a hook-detached subtree
|
|
1727
|
-
can still hold a queued resource-event handler - e.g. an <img onload>
|
|
1728
|
-
that began loading when the caller built the live tree - which fires
|
|
1729
|
-
in page scope after sanitize returns even though the handler never
|
|
1730
|
-
reached the returned tree. That is the audit-5 F1 hazard, and the
|
|
1731
|
-
documented node.remove() hook pattern walks straight into it. So on
|
|
1732
|
-
the IN_PLACE path we neutralize the detached subtree inline here,
|
|
1733
|
-
stripping its non-allow-listed attributes before returning, exactly
|
|
1734
|
-
as the post-walk pass does for _forceRemove'd subtrees. */
|
|
1735
|
-
if (currentNode !== root && getParentNode(currentNode) === null) {
|
|
1736
|
-
if (IN_PLACE) {
|
|
1737
|
-
_neutralizeSubtree(currentNode);
|
|
1738
|
-
}
|
|
1883
|
+
/* The uponSanitizeElement hook may have detached the node, exactly as
|
|
1884
|
+
above (see _handleHookDetachedNode for the full rationale). */
|
|
1885
|
+
if (_handleHookDetachedNode(currentNode, root)) {
|
|
1739
1886
|
return true;
|
|
1740
1887
|
}
|
|
1741
1888
|
/* Remove mXSS vectors, processing instructions and risky comments */
|
|
@@ -1768,7 +1915,7 @@ function createDOMPurify() {
|
|
|
1768
1915
|
bound and short-circuits to false for any node minted in a different
|
|
1769
1916
|
realm — letting a foreign-realm element with a forbidden namespace
|
|
1770
1917
|
slip past the namespace check entirely. */
|
|
1771
|
-
const nt =
|
|
1918
|
+
const nt = _readNodeType(currentNode);
|
|
1772
1919
|
if (nt === NODE_TYPE.element && !_checkValidNamespace(currentNode)) {
|
|
1773
1920
|
_forceRemove(currentNode);
|
|
1774
1921
|
return true;
|
|
@@ -1828,10 +1975,7 @@ function createDOMPurify() {
|
|
|
1828
1975
|
other structural-threat checks and stays overridable, consistent with
|
|
1829
1976
|
the rest of the codebase. PI range markers are already removed by
|
|
1830
1977
|
_isUnsafeNode. */
|
|
1831
|
-
if (
|
|
1832
|
-
return false;
|
|
1833
|
-
}
|
|
1834
|
-
if (SAFE_FOR_XML && lcName === 'for' && lcTag !== 'label' && lcTag !== 'output') {
|
|
1978
|
+
if (_isPatchLinkageAttribute(lcName, lcTag)) {
|
|
1835
1979
|
return false;
|
|
1836
1980
|
}
|
|
1837
1981
|
/* Make sure attribute cannot clobber */
|
|
@@ -1843,22 +1987,54 @@ function createDOMPurify() {
|
|
|
1843
1987
|
(https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes)
|
|
1844
1988
|
XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804)
|
|
1845
1989
|
We don't need to check the value; it's always URI safe. */
|
|
1846
|
-
if (ALLOW_DATA_ATTR && regExpTest(DATA_ATTR$1, lcName))
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
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;
|
|
1862
2038
|
};
|
|
1863
2039
|
/* Names the HTML spec reserves from valid-custom-element-name; these must
|
|
1864
2040
|
* never be treated as basic custom elements even when a permissive
|
|
@@ -1983,7 +2159,7 @@ function createDOMPurify() {
|
|
|
1983
2159
|
*/
|
|
1984
2160
|
if (SANITIZE_NAMED_PROPS && (lcName === 'id' || lcName === 'name') && stringIndexOf(value, SANITIZE_NAMED_PROPS_PREFIX) !== 0) {
|
|
1985
2161
|
// Remove the attribute with this value
|
|
1986
|
-
_removeAttribute(name, currentNode);
|
|
2162
|
+
_removeAttribute(name, currentNode, attr);
|
|
1987
2163
|
// Prefix the value and later re-create the attribute with the sanitized value
|
|
1988
2164
|
value = SANITIZE_NAMED_PROPS_PREFIX + value;
|
|
1989
2165
|
}
|
|
@@ -1991,12 +2167,12 @@ function createDOMPurify() {
|
|
|
1991
2167
|
// itself the clobbering protection, and re-applying it is incorrect.
|
|
1992
2168
|
/* Work around a security issue with comments inside attributes */
|
|
1993
2169
|
if (SAFE_FOR_XML && regExpTest(/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i, value)) {
|
|
1994
|
-
_removeAttribute(name, currentNode);
|
|
2170
|
+
_removeAttribute(name, currentNode, attr);
|
|
1995
2171
|
continue;
|
|
1996
2172
|
}
|
|
1997
2173
|
/* Make sure we cannot easily use animated hrefs, even if animations are allowed */
|
|
1998
2174
|
if (lcName === 'attributename' && stringMatch(value, 'href')) {
|
|
1999
|
-
_removeAttribute(name, currentNode);
|
|
2175
|
+
_removeAttribute(name, currentNode, attr);
|
|
2000
2176
|
continue;
|
|
2001
2177
|
}
|
|
2002
2178
|
/* Did the hooks force-keep the attribute? */
|
|
@@ -2005,12 +2181,12 @@ function createDOMPurify() {
|
|
|
2005
2181
|
}
|
|
2006
2182
|
/* Did the hooks approve of the attribute? */
|
|
2007
2183
|
if (!hookEvent.keepAttr) {
|
|
2008
|
-
_removeAttribute(name, currentNode);
|
|
2184
|
+
_removeAttribute(name, currentNode, attr);
|
|
2009
2185
|
continue;
|
|
2010
2186
|
}
|
|
2011
2187
|
/* Work around a security issue in jQuery 3.0 */
|
|
2012
2188
|
if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(SELF_CLOSING_TAG, value)) {
|
|
2013
|
-
_removeAttribute(name, currentNode);
|
|
2189
|
+
_removeAttribute(name, currentNode, attr);
|
|
2014
2190
|
continue;
|
|
2015
2191
|
}
|
|
2016
2192
|
/* Sanitize attribute content to be template-safe */
|
|
@@ -2019,7 +2195,7 @@ function createDOMPurify() {
|
|
|
2019
2195
|
}
|
|
2020
2196
|
/* Is `value` valid for this attribute? */
|
|
2021
2197
|
if (!_isValidAttribute(lcTag, lcName, value)) {
|
|
2022
|
-
_removeAttribute(name, currentNode);
|
|
2198
|
+
_removeAttribute(name, currentNode, attr);
|
|
2023
2199
|
continue;
|
|
2024
2200
|
}
|
|
2025
2201
|
/* Handle attributes that require Trusted Types */
|
|
@@ -2066,8 +2242,7 @@ function createDOMPurify() {
|
|
|
2066
2242
|
Walk it explicitly. The nodeType guard avoids reading
|
|
2067
2243
|
shadowRoot off text / comment / CDATA / PI nodes that the
|
|
2068
2244
|
iterator also surfaces. */
|
|
2069
|
-
|
|
2070
|
-
if (shadowNodeType === NODE_TYPE.element) {
|
|
2245
|
+
if (_readNodeType(shadowNode) === NODE_TYPE.element) {
|
|
2071
2246
|
const innerSr = getShadowRoot(shadowNode);
|
|
2072
2247
|
if (_isDocumentFragment(innerSr)) {
|
|
2073
2248
|
_sanitizeAttachedShadowRoots(innerSr);
|
|
@@ -2125,7 +2300,7 @@ function createDOMPurify() {
|
|
|
2125
2300
|
continue;
|
|
2126
2301
|
}
|
|
2127
2302
|
const node = item.node;
|
|
2128
|
-
const nodeType =
|
|
2303
|
+
const nodeType = _readNodeType(node);
|
|
2129
2304
|
const isElement = nodeType === NODE_TYPE.element;
|
|
2130
2305
|
/* (pushed last → processed first) Children, snapshotted in reverse so
|
|
2131
2306
|
the first child is processed first. Snapshotting matters because a
|
|
@@ -2247,7 +2422,7 @@ function createDOMPurify() {
|
|
|
2247
2422
|
child named "nodeName" on the form root would otherwise shadow
|
|
2248
2423
|
the property and let this check skip the root-allowlist
|
|
2249
2424
|
validation entirely. */
|
|
2250
|
-
const nn =
|
|
2425
|
+
const nn = _readNodeName(dirty);
|
|
2251
2426
|
if (typeof nn === 'string') {
|
|
2252
2427
|
const tagName = transformCaseFunc(nn);
|
|
2253
2428
|
if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {
|