dompurify 3.4.9 → 3.4.11
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 +36 -13
- package/dist/purify.cjs.d.ts +3 -3
- package/dist/purify.cjs.js +359 -190
- package/dist/purify.cjs.js.map +1 -1
- package/dist/purify.cov.cjs.js +26427 -0
- package/dist/purify.cov.cjs.js.map +1 -0
- package/dist/purify.es.d.mts +3 -3
- package/dist/purify.es.mjs +359 -190
- package/dist/purify.es.mjs.map +1 -1
- package/dist/purify.js +359 -190
- 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 +23 -8
- package/src/config.ts +0 -2
- package/src/purify.ts +498 -656
- package/src/regexp.ts +8 -0
- package/src/types.ts +354 -0
package/dist/purify.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! @license DOMPurify 3.4.
|
|
1
|
+
/*! @license DOMPurify 3.4.11 | (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.11/LICENSE */
|
|
2
2
|
|
|
3
3
|
(function (global, factory) {
|
|
4
4
|
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
|
@@ -333,8 +333,14 @@
|
|
|
333
333
|
);
|
|
334
334
|
const DOCTYPE_NAME = seal(/^html$/i);
|
|
335
335
|
const CUSTOM_ELEMENT = seal(/^[a-z][.\w]*(-[.\w]+)+$/i);
|
|
336
|
+
// Markup-significant character probes used by _sanitizeElements.
|
|
337
|
+
// Shared module-level instances are safe despite the sticky /g flags:
|
|
338
|
+
// unapply() resets lastIndex for RegExp receivers before every call.
|
|
339
|
+
const ELEMENT_MARKUP_PROBE = seal(/<[/\w!]/g);
|
|
340
|
+
const COMMENT_MARKUP_PROBE = seal(/<[/\w]/g);
|
|
341
|
+
const FALLBACK_TAG_CLOSE = seal(/<\/no(script|embed|frames)/i);
|
|
342
|
+
const SELF_CLOSING_TAG = seal(/\/>/i);
|
|
336
343
|
|
|
337
|
-
/* eslint-disable @typescript-eslint/indent */
|
|
338
344
|
// https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
|
|
339
345
|
const NODE_TYPE = {
|
|
340
346
|
element: 1,
|
|
@@ -345,7 +351,7 @@
|
|
|
345
351
|
// Deprecated
|
|
346
352
|
entityNode: 6,
|
|
347
353
|
// Deprecated
|
|
348
|
-
|
|
354
|
+
processingInstruction: 7,
|
|
349
355
|
comment: 8,
|
|
350
356
|
document: 9,
|
|
351
357
|
documentType: 10,
|
|
@@ -406,10 +412,25 @@
|
|
|
406
412
|
uponSanitizeShadowNode: []
|
|
407
413
|
};
|
|
408
414
|
};
|
|
415
|
+
/**
|
|
416
|
+
* Resolve a set-valued configuration option: a fresh set built from
|
|
417
|
+
* cfg[key] when it is an own array property (seeded with a clone of
|
|
418
|
+
* options.base when given, case-normalized via options.transform),
|
|
419
|
+
* the fallback set otherwise.
|
|
420
|
+
*
|
|
421
|
+
* @param cfg the cloned, prototype-free configuration object
|
|
422
|
+
* @param key the configuration property to read
|
|
423
|
+
* @param fallback the set to use when the option is absent or not an array
|
|
424
|
+
* @param options transform and optional base set to merge into
|
|
425
|
+
* @returns the resolved set
|
|
426
|
+
*/
|
|
427
|
+
const _resolveSetOption = function _resolveSetOption(cfg, key, fallback, options) {
|
|
428
|
+
return objectHasOwnProperty(cfg, key) && arrayIsArray(cfg[key]) ? addToSet(options.base ? clone(options.base) : {}, cfg[key], options.transform) : fallback;
|
|
429
|
+
};
|
|
409
430
|
function createDOMPurify() {
|
|
410
431
|
let window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal();
|
|
411
432
|
const DOMPurify = root => createDOMPurify(root);
|
|
412
|
-
DOMPurify.version = '3.4.
|
|
433
|
+
DOMPurify.version = '3.4.11';
|
|
413
434
|
DOMPurify.removed = [];
|
|
414
435
|
if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) {
|
|
415
436
|
// Not running in a browser, provide a factory function
|
|
@@ -598,6 +619,13 @@
|
|
|
598
619
|
let WHOLE_DOCUMENT = false;
|
|
599
620
|
/* Track whether config is already set on this instance of DOMPurify. */
|
|
600
621
|
let SET_CONFIG = false;
|
|
622
|
+
/* Pristine allowlist bindings captured at setConfig() time. On the
|
|
623
|
+
* persistent-config path sanitize() restores the sets from these before
|
|
624
|
+
* the per-walk hook clone-guard, so a hook's in-call widening cannot
|
|
625
|
+
* carry across calls. Null until setConfig() is called; reset by
|
|
626
|
+
* clearConfig(). */
|
|
627
|
+
let SET_CONFIG_ALLOWED_TAGS = null;
|
|
628
|
+
let SET_CONFIG_ALLOWED_ATTR = null;
|
|
601
629
|
/* Decide if all elements (e.g. style, script) must be children of
|
|
602
630
|
* document.body. By default, browsers might move them to document.head */
|
|
603
631
|
let FORCE_BODY = false;
|
|
@@ -666,8 +694,10 @@
|
|
|
666
694
|
/* Allowed XHTML+XML namespaces */
|
|
667
695
|
let ALLOWED_NAMESPACES = null;
|
|
668
696
|
const DEFAULT_ALLOWED_NAMESPACES = addToSet({}, [MATHML_NAMESPACE, SVG_NAMESPACE, HTML_NAMESPACE], stringToString);
|
|
669
|
-
|
|
670
|
-
let
|
|
697
|
+
const DEFAULT_MATHML_TEXT_INTEGRATION_POINTS = freeze(['mi', 'mo', 'mn', 'ms', 'mtext']);
|
|
698
|
+
let MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, DEFAULT_MATHML_TEXT_INTEGRATION_POINTS);
|
|
699
|
+
const DEFAULT_HTML_INTEGRATION_POINTS = freeze(['annotation-xml']);
|
|
700
|
+
let HTML_INTEGRATION_POINTS = addToSet({}, DEFAULT_HTML_INTEGRATION_POINTS);
|
|
671
701
|
// Certain elements are allowed in both SVG and HTML
|
|
672
702
|
// namespace. We need to specify them explicitly
|
|
673
703
|
// so that they don't get erroneously deleted from
|
|
@@ -709,14 +739,32 @@
|
|
|
709
739
|
// HTML tags and attributes are not case-sensitive, converting to lowercase. Keeping XHTML as is.
|
|
710
740
|
transformCaseFunc = PARSER_MEDIA_TYPE === 'application/xhtml+xml' ? stringToString : stringToLowerCase;
|
|
711
741
|
/* Set configuration parameters */
|
|
712
|
-
ALLOWED_TAGS =
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
742
|
+
ALLOWED_TAGS = _resolveSetOption(cfg, 'ALLOWED_TAGS', DEFAULT_ALLOWED_TAGS, {
|
|
743
|
+
transform: transformCaseFunc
|
|
744
|
+
});
|
|
745
|
+
ALLOWED_ATTR = _resolveSetOption(cfg, 'ALLOWED_ATTR', DEFAULT_ALLOWED_ATTR, {
|
|
746
|
+
transform: transformCaseFunc
|
|
747
|
+
});
|
|
748
|
+
ALLOWED_NAMESPACES = _resolveSetOption(cfg, 'ALLOWED_NAMESPACES', DEFAULT_ALLOWED_NAMESPACES, {
|
|
749
|
+
transform: stringToString
|
|
750
|
+
});
|
|
751
|
+
URI_SAFE_ATTRIBUTES = _resolveSetOption(cfg, 'ADD_URI_SAFE_ATTR', DEFAULT_URI_SAFE_ATTRIBUTES, {
|
|
752
|
+
transform: transformCaseFunc,
|
|
753
|
+
base: DEFAULT_URI_SAFE_ATTRIBUTES
|
|
754
|
+
});
|
|
755
|
+
DATA_URI_TAGS = _resolveSetOption(cfg, 'ADD_DATA_URI_TAGS', DEFAULT_DATA_URI_TAGS, {
|
|
756
|
+
transform: transformCaseFunc,
|
|
757
|
+
base: DEFAULT_DATA_URI_TAGS
|
|
758
|
+
});
|
|
759
|
+
FORBID_CONTENTS = _resolveSetOption(cfg, 'FORBID_CONTENTS', DEFAULT_FORBID_CONTENTS, {
|
|
760
|
+
transform: transformCaseFunc
|
|
761
|
+
});
|
|
762
|
+
FORBID_TAGS = _resolveSetOption(cfg, 'FORBID_TAGS', clone({}), {
|
|
763
|
+
transform: transformCaseFunc
|
|
764
|
+
});
|
|
765
|
+
FORBID_ATTR = _resolveSetOption(cfg, 'FORBID_ATTR', clone({}), {
|
|
766
|
+
transform: transformCaseFunc
|
|
767
|
+
});
|
|
720
768
|
USE_PROFILES = objectHasOwnProperty(cfg, 'USE_PROFILES') ? cfg.USE_PROFILES && typeof cfg.USE_PROFILES === 'object' ? clone(cfg.USE_PROFILES) : cfg.USE_PROFILES : false;
|
|
721
769
|
ALLOW_ARIA_ATTR = cfg.ALLOW_ARIA_ATTR !== false; // Default true
|
|
722
770
|
ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR !== false; // Default true
|
|
@@ -735,8 +783,8 @@
|
|
|
735
783
|
IN_PLACE = cfg.IN_PLACE || false; // Default false
|
|
736
784
|
IS_ALLOWED_URI$1 = isRegex(cfg.ALLOWED_URI_REGEXP) ? cfg.ALLOWED_URI_REGEXP : IS_ALLOWED_URI; // Default regexp
|
|
737
785
|
NAMESPACE = typeof cfg.NAMESPACE === 'string' ? cfg.NAMESPACE : HTML_NAMESPACE; // Default HTML namespace
|
|
738
|
-
MATHML_TEXT_INTEGRATION_POINTS = objectHasOwnProperty(cfg, 'MATHML_TEXT_INTEGRATION_POINTS') && cfg.MATHML_TEXT_INTEGRATION_POINTS && typeof cfg.MATHML_TEXT_INTEGRATION_POINTS === 'object' ? clone(cfg.MATHML_TEXT_INTEGRATION_POINTS) : addToSet({},
|
|
739
|
-
HTML_INTEGRATION_POINTS = objectHasOwnProperty(cfg, 'HTML_INTEGRATION_POINTS') && cfg.HTML_INTEGRATION_POINTS && typeof cfg.HTML_INTEGRATION_POINTS === 'object' ? clone(cfg.HTML_INTEGRATION_POINTS) : addToSet({},
|
|
786
|
+
MATHML_TEXT_INTEGRATION_POINTS = objectHasOwnProperty(cfg, 'MATHML_TEXT_INTEGRATION_POINTS') && cfg.MATHML_TEXT_INTEGRATION_POINTS && typeof cfg.MATHML_TEXT_INTEGRATION_POINTS === 'object' ? clone(cfg.MATHML_TEXT_INTEGRATION_POINTS) : addToSet({}, DEFAULT_MATHML_TEXT_INTEGRATION_POINTS); // Default built-in map
|
|
787
|
+
HTML_INTEGRATION_POINTS = objectHasOwnProperty(cfg, 'HTML_INTEGRATION_POINTS') && cfg.HTML_INTEGRATION_POINTS && typeof cfg.HTML_INTEGRATION_POINTS === 'object' ? clone(cfg.HTML_INTEGRATION_POINTS) : addToSet({}, DEFAULT_HTML_INTEGRATION_POINTS); // Default built-in map
|
|
740
788
|
const customElementHandling = objectHasOwnProperty(cfg, 'CUSTOM_ELEMENT_HANDLING') && cfg.CUSTOM_ELEMENT_HANDLING && typeof cfg.CUSTOM_ELEMENT_HANDLING === 'object' ? clone(cfg.CUSTOM_ELEMENT_HANDLING) : create(null);
|
|
741
789
|
CUSTOM_ELEMENT_HANDLING = create(null);
|
|
742
790
|
if (objectHasOwnProperty(customElementHandling, 'tagNameCheck') && isRegexOrFunction(customElementHandling.tagNameCheck)) {
|
|
@@ -748,6 +796,7 @@
|
|
|
748
796
|
if (objectHasOwnProperty(customElementHandling, 'allowCustomizedBuiltInElements') && typeof customElementHandling.allowCustomizedBuiltInElements === 'boolean') {
|
|
749
797
|
CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements = customElementHandling.allowCustomizedBuiltInElements; // Default undefined
|
|
750
798
|
}
|
|
799
|
+
seal(CUSTOM_ELEMENT_HANDLING);
|
|
751
800
|
if (SAFE_FOR_TEMPLATES) {
|
|
752
801
|
ALLOW_DATA_ATTR = false;
|
|
753
802
|
}
|
|
@@ -886,21 +935,6 @@
|
|
|
886
935
|
emptyHTML = _createTrustedHTML('');
|
|
887
936
|
}
|
|
888
937
|
}
|
|
889
|
-
/*
|
|
890
|
-
* Mirror the clone-before-mutate pattern already applied above for
|
|
891
|
-
* cfg.ADD_TAGS / cfg.ADD_ATTR: if any uponSanitize* hook is
|
|
892
|
-
* registered AND the set still points at the default constant,
|
|
893
|
-
* clone it. The hook then mutates the clone (in-call widening
|
|
894
|
-
* still works exactly as documented) and the next default-cfg
|
|
895
|
-
* call rebinds to the untouched original via the reassignment at
|
|
896
|
-
* the top of this function.
|
|
897
|
-
*/
|
|
898
|
-
if ((hooks.uponSanitizeElement.length > 0 || hooks.uponSanitizeAttribute.length > 0) && ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) {
|
|
899
|
-
ALLOWED_TAGS = clone(ALLOWED_TAGS);
|
|
900
|
-
}
|
|
901
|
-
if (hooks.uponSanitizeAttribute.length > 0 && ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) {
|
|
902
|
-
ALLOWED_ATTR = clone(ALLOWED_ATTR);
|
|
903
|
-
}
|
|
904
938
|
// Prevent further manipulation of configuration.
|
|
905
939
|
// Not available in IE8, Safari 5, etc.
|
|
906
940
|
if (freeze) {
|
|
@@ -913,6 +947,77 @@
|
|
|
913
947
|
* correctly. */
|
|
914
948
|
const ALL_SVG_TAGS = addToSet({}, [...svg$1, ...svgFilters, ...svgDisallowed]);
|
|
915
949
|
const ALL_MATHML_TAGS = addToSet({}, [...mathMl$1, ...mathMlDisallowed]);
|
|
950
|
+
/**
|
|
951
|
+
* Namespace rules for an element in the SVG namespace.
|
|
952
|
+
*
|
|
953
|
+
* @param tagName the element's lowercase tag name
|
|
954
|
+
* @param parent the (possibly simulated) parent node
|
|
955
|
+
* @param parentTagName the parent's lowercase tag name
|
|
956
|
+
* @returns true if a spec-compliant parser could produce this element
|
|
957
|
+
*/
|
|
958
|
+
const _checkSvgNamespace = function _checkSvgNamespace(tagName, parent, parentTagName) {
|
|
959
|
+
// The only way to switch from HTML namespace to SVG
|
|
960
|
+
// is via <svg>. If it happens via any other tag, then
|
|
961
|
+
// it should be killed.
|
|
962
|
+
if (parent.namespaceURI === HTML_NAMESPACE) {
|
|
963
|
+
return tagName === 'svg';
|
|
964
|
+
}
|
|
965
|
+
// The only way to switch from MathML to SVG is via <svg>
|
|
966
|
+
// if the parent is either <annotation-xml> or a MathML
|
|
967
|
+
// text integration point.
|
|
968
|
+
if (parent.namespaceURI === MATHML_NAMESPACE) {
|
|
969
|
+
return tagName === 'svg' && (parentTagName === 'annotation-xml' || MATHML_TEXT_INTEGRATION_POINTS[parentTagName]);
|
|
970
|
+
}
|
|
971
|
+
// We only allow elements that are defined in SVG
|
|
972
|
+
// spec. All others are disallowed in SVG namespace.
|
|
973
|
+
return Boolean(ALL_SVG_TAGS[tagName]);
|
|
974
|
+
};
|
|
975
|
+
/**
|
|
976
|
+
* Namespace rules for an element in the MathML namespace.
|
|
977
|
+
*
|
|
978
|
+
* @param tagName the element's lowercase tag name
|
|
979
|
+
* @param parent the (possibly simulated) parent node
|
|
980
|
+
* @param parentTagName the parent's lowercase tag name
|
|
981
|
+
* @returns true if a spec-compliant parser could produce this element
|
|
982
|
+
*/
|
|
983
|
+
const _checkMathMlNamespace = function _checkMathMlNamespace(tagName, parent, parentTagName) {
|
|
984
|
+
// The only way to switch from HTML namespace to MathML
|
|
985
|
+
// is via <math>. If it happens via any other tag, then
|
|
986
|
+
// it should be killed.
|
|
987
|
+
if (parent.namespaceURI === HTML_NAMESPACE) {
|
|
988
|
+
return tagName === 'math';
|
|
989
|
+
}
|
|
990
|
+
// The only way to switch from SVG to MathML is via
|
|
991
|
+
// <math> and HTML integration points
|
|
992
|
+
if (parent.namespaceURI === SVG_NAMESPACE) {
|
|
993
|
+
return tagName === 'math' && HTML_INTEGRATION_POINTS[parentTagName];
|
|
994
|
+
}
|
|
995
|
+
// We only allow elements that are defined in MathML
|
|
996
|
+
// spec. All others are disallowed in MathML namespace.
|
|
997
|
+
return Boolean(ALL_MATHML_TAGS[tagName]);
|
|
998
|
+
};
|
|
999
|
+
/**
|
|
1000
|
+
* Namespace rules for an element in the HTML namespace.
|
|
1001
|
+
*
|
|
1002
|
+
* @param tagName the element's lowercase tag name
|
|
1003
|
+
* @param parent the (possibly simulated) parent node
|
|
1004
|
+
* @param parentTagName the parent's lowercase tag name
|
|
1005
|
+
* @returns true if a spec-compliant parser could produce this element
|
|
1006
|
+
*/
|
|
1007
|
+
const _checkHtmlNamespace = function _checkHtmlNamespace(tagName, parent, parentTagName) {
|
|
1008
|
+
// The only way to switch from SVG to HTML is via
|
|
1009
|
+
// HTML integration points, and from MathML to HTML
|
|
1010
|
+
// is via MathML text integration points
|
|
1011
|
+
if (parent.namespaceURI === SVG_NAMESPACE && !HTML_INTEGRATION_POINTS[parentTagName]) {
|
|
1012
|
+
return false;
|
|
1013
|
+
}
|
|
1014
|
+
if (parent.namespaceURI === MATHML_NAMESPACE && !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) {
|
|
1015
|
+
return false;
|
|
1016
|
+
}
|
|
1017
|
+
// We disallow tags that are specific for MathML
|
|
1018
|
+
// or SVG and should never appear in HTML namespace
|
|
1019
|
+
return !ALL_MATHML_TAGS[tagName] && (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName]);
|
|
1020
|
+
};
|
|
916
1021
|
/**
|
|
917
1022
|
* @param element a DOM element whose namespace is being checked
|
|
918
1023
|
* @returns Return false if the element has a
|
|
@@ -935,51 +1040,13 @@
|
|
|
935
1040
|
return false;
|
|
936
1041
|
}
|
|
937
1042
|
if (element.namespaceURI === SVG_NAMESPACE) {
|
|
938
|
-
|
|
939
|
-
// is via <svg>. If it happens via any other tag, then
|
|
940
|
-
// it should be killed.
|
|
941
|
-
if (parent.namespaceURI === HTML_NAMESPACE) {
|
|
942
|
-
return tagName === 'svg';
|
|
943
|
-
}
|
|
944
|
-
// The only way to switch from MathML to SVG is via`
|
|
945
|
-
// svg if parent is either <annotation-xml> or MathML
|
|
946
|
-
// text integration points.
|
|
947
|
-
if (parent.namespaceURI === MATHML_NAMESPACE) {
|
|
948
|
-
return tagName === 'svg' && (parentTagName === 'annotation-xml' || MATHML_TEXT_INTEGRATION_POINTS[parentTagName]);
|
|
949
|
-
}
|
|
950
|
-
// We only allow elements that are defined in SVG
|
|
951
|
-
// spec. All others are disallowed in SVG namespace.
|
|
952
|
-
return Boolean(ALL_SVG_TAGS[tagName]);
|
|
1043
|
+
return _checkSvgNamespace(tagName, parent, parentTagName);
|
|
953
1044
|
}
|
|
954
1045
|
if (element.namespaceURI === MATHML_NAMESPACE) {
|
|
955
|
-
|
|
956
|
-
// is via <math>. If it happens via any other tag, then
|
|
957
|
-
// it should be killed.
|
|
958
|
-
if (parent.namespaceURI === HTML_NAMESPACE) {
|
|
959
|
-
return tagName === 'math';
|
|
960
|
-
}
|
|
961
|
-
// The only way to switch from SVG to MathML is via
|
|
962
|
-
// <math> and HTML integration points
|
|
963
|
-
if (parent.namespaceURI === SVG_NAMESPACE) {
|
|
964
|
-
return tagName === 'math' && HTML_INTEGRATION_POINTS[parentTagName];
|
|
965
|
-
}
|
|
966
|
-
// We only allow elements that are defined in MathML
|
|
967
|
-
// spec. All others are disallowed in MathML namespace.
|
|
968
|
-
return Boolean(ALL_MATHML_TAGS[tagName]);
|
|
1046
|
+
return _checkMathMlNamespace(tagName, parent, parentTagName);
|
|
969
1047
|
}
|
|
970
1048
|
if (element.namespaceURI === HTML_NAMESPACE) {
|
|
971
|
-
|
|
972
|
-
// HTML integration points, and from MathML to HTML
|
|
973
|
-
// is via MathML text integration points
|
|
974
|
-
if (parent.namespaceURI === SVG_NAMESPACE && !HTML_INTEGRATION_POINTS[parentTagName]) {
|
|
975
|
-
return false;
|
|
976
|
-
}
|
|
977
|
-
if (parent.namespaceURI === MATHML_NAMESPACE && !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) {
|
|
978
|
-
return false;
|
|
979
|
-
}
|
|
980
|
-
// We disallow tags that are specific for MathML
|
|
981
|
-
// or SVG and should never appear in HTML namespace
|
|
982
|
-
return !ALL_MATHML_TAGS[tagName] && (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName]);
|
|
1049
|
+
return _checkHtmlNamespace(tagName, parent, parentTagName);
|
|
983
1050
|
}
|
|
984
1051
|
// For XHTML and XML documents that support custom namespaces
|
|
985
1052
|
if (PARSER_MEDIA_TYPE === 'application/xhtml+xml' && ALLOWED_NAMESPACES[element.namespaceURI]) {
|
|
@@ -1045,7 +1112,7 @@
|
|
|
1045
1112
|
* @param root the in-place root to empty
|
|
1046
1113
|
*/
|
|
1047
1114
|
const _neutralizeRoot = function _neutralizeRoot(root) {
|
|
1048
|
-
const childNodes = getChildNodes
|
|
1115
|
+
const childNodes = getChildNodes(root);
|
|
1049
1116
|
if (childNodes) {
|
|
1050
1117
|
const snapshot = [];
|
|
1051
1118
|
arrayForEach(childNodes, child => {
|
|
@@ -1059,7 +1126,7 @@
|
|
|
1059
1126
|
}
|
|
1060
1127
|
});
|
|
1061
1128
|
}
|
|
1062
|
-
const attributes = getAttributes
|
|
1129
|
+
const attributes = getAttributes(root);
|
|
1063
1130
|
if (attributes) {
|
|
1064
1131
|
for (let i = attributes.length - 1; i >= 0; --i) {
|
|
1065
1132
|
const attribute = attributes[i];
|
|
@@ -1117,7 +1184,7 @@
|
|
|
1117
1184
|
* @param element the element to strip
|
|
1118
1185
|
*/
|
|
1119
1186
|
const _stripDisallowedAttributes = function _stripDisallowedAttributes(element) {
|
|
1120
|
-
const attributes = getAttributes
|
|
1187
|
+
const attributes = getAttributes(element);
|
|
1121
1188
|
if (!attributes) {
|
|
1122
1189
|
return;
|
|
1123
1190
|
}
|
|
@@ -1164,7 +1231,7 @@
|
|
|
1164
1231
|
if (nodeType === NODE_TYPE.element) {
|
|
1165
1232
|
_stripDisallowedAttributes(node);
|
|
1166
1233
|
}
|
|
1167
|
-
const childNodes = getChildNodes
|
|
1234
|
+
const childNodes = getChildNodes(node);
|
|
1168
1235
|
if (childNodes) {
|
|
1169
1236
|
for (let i = childNodes.length - 1; i >= 0; --i) {
|
|
1170
1237
|
stack.push(childNodes[i]);
|
|
@@ -1233,6 +1300,20 @@
|
|
|
1233
1300
|
// eslint-disable-next-line no-bitwise
|
|
1234
1301
|
NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_PROCESSING_INSTRUCTION | NodeFilter.SHOW_CDATA_SECTION, null);
|
|
1235
1302
|
};
|
|
1303
|
+
/**
|
|
1304
|
+
* Replace template expression syntax (mustache, ERB, template
|
|
1305
|
+
* literal) with a space; shared by all SAFE_FOR_TEMPLATES scrub
|
|
1306
|
+
* sites. Order matters: mustache, then ERB, then template literal.
|
|
1307
|
+
*
|
|
1308
|
+
* @param value the string to scrub
|
|
1309
|
+
* @returns the scrubbed string
|
|
1310
|
+
*/
|
|
1311
|
+
const _stripTemplateExpressions = function _stripTemplateExpressions(value) {
|
|
1312
|
+
value = stringReplace(value, MUSTACHE_EXPR$1, ' ');
|
|
1313
|
+
value = stringReplace(value, ERB_EXPR$1, ' ');
|
|
1314
|
+
value = stringReplace(value, TMPLIT_EXPR$1, ' ');
|
|
1315
|
+
return value;
|
|
1316
|
+
};
|
|
1236
1317
|
/**
|
|
1237
1318
|
* Strip template-engine expressions ({{...}}, ${...}, <%...%>) from the
|
|
1238
1319
|
* character data of an element subtree. Used as the final safety net for
|
|
@@ -1253,29 +1334,27 @@
|
|
|
1253
1334
|
* @param node The root element whose character data should be scrubbed.
|
|
1254
1335
|
*/
|
|
1255
1336
|
const _scrubTemplateExpressions2 = function _scrubTemplateExpressions(node) {
|
|
1256
|
-
var _node$querySelectorAl
|
|
1337
|
+
var _node$querySelectorAl;
|
|
1257
1338
|
node.normalize();
|
|
1258
1339
|
const walker = createNodeIterator.call(node.ownerDocument || node, node,
|
|
1259
1340
|
// eslint-disable-next-line no-bitwise
|
|
1260
1341
|
NodeFilter.SHOW_TEXT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_CDATA_SECTION | NodeFilter.SHOW_PROCESSING_INSTRUCTION, null);
|
|
1261
1342
|
let currentNode = walker.nextNode();
|
|
1262
1343
|
while (currentNode) {
|
|
1263
|
-
|
|
1264
|
-
arrayForEach([MUSTACHE_EXPR$1, ERB_EXPR$1, TMPLIT_EXPR$1], expr => {
|
|
1265
|
-
data = stringReplace(data, expr, ' ');
|
|
1266
|
-
});
|
|
1267
|
-
currentNode.data = data;
|
|
1344
|
+
currentNode.data = _stripTemplateExpressions(currentNode.data);
|
|
1268
1345
|
currentNode = walker.nextNode();
|
|
1269
1346
|
}
|
|
1270
1347
|
// NodeIterator does not descend into <template>.content per the DOM spec,
|
|
1271
1348
|
// so we must explicitly recurse into each template's content fragment,
|
|
1272
1349
|
// mirroring the approach used by _sanitizeShadowDOM.
|
|
1273
|
-
const templates = (_node$querySelectorAl =
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1350
|
+
const templates = (_node$querySelectorAl = node.querySelectorAll) === null || _node$querySelectorAl === void 0 ? void 0 : _node$querySelectorAl.call(node, 'template');
|
|
1351
|
+
if (templates) {
|
|
1352
|
+
arrayForEach(templates, tmpl => {
|
|
1353
|
+
if (_isDocumentFragment(tmpl.content)) {
|
|
1354
|
+
_scrubTemplateExpressions2(tmpl.content);
|
|
1355
|
+
}
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1279
1358
|
};
|
|
1280
1359
|
/**
|
|
1281
1360
|
* _isClobbered
|
|
@@ -1371,67 +1450,64 @@
|
|
|
1371
1450
|
}
|
|
1372
1451
|
};
|
|
1373
1452
|
function _executeHooks(hooks, currentNode, data) {
|
|
1453
|
+
if (hooks.length === 0) {
|
|
1454
|
+
return;
|
|
1455
|
+
}
|
|
1374
1456
|
arrayForEach(hooks, hook => {
|
|
1375
1457
|
hook.call(DOMPurify, currentNode, data, CONFIG);
|
|
1376
1458
|
});
|
|
1377
1459
|
}
|
|
1378
1460
|
/**
|
|
1379
|
-
*
|
|
1461
|
+
* Structural-threat checks that condemn a node regardless of the
|
|
1462
|
+
* allowlists: mXSS via namespace confusion, risky CSS construction,
|
|
1463
|
+
* processing instructions, markup-bearing comments. Pure predicate;
|
|
1464
|
+
* the caller removes. Check order is load-bearing.
|
|
1380
1465
|
*
|
|
1381
|
-
* @
|
|
1382
|
-
* @
|
|
1383
|
-
* @
|
|
1384
|
-
* @param currentNode to check for permission to exist
|
|
1385
|
-
* @return true if node was killed, false if left alive
|
|
1466
|
+
* @param currentNode the node to inspect
|
|
1467
|
+
* @param tagName the node's transformCaseFunc'd tag name
|
|
1468
|
+
* @return true if the node must be removed
|
|
1386
1469
|
*/
|
|
1387
|
-
const
|
|
1388
|
-
let content = null;
|
|
1389
|
-
/* Execute a hook if present */
|
|
1390
|
-
_executeHooks(hooks.beforeSanitizeElements, currentNode, null);
|
|
1391
|
-
/* Check if element is clobbered or can clobber */
|
|
1392
|
-
if (_isClobbered(currentNode)) {
|
|
1393
|
-
_forceRemove(currentNode);
|
|
1394
|
-
return true;
|
|
1395
|
-
}
|
|
1396
|
-
/* Now let's check the element's type and name */
|
|
1397
|
-
const tagName = transformCaseFunc(getNodeName ? getNodeName(currentNode) : currentNode.nodeName);
|
|
1398
|
-
/* Execute a hook if present */
|
|
1399
|
-
_executeHooks(hooks.uponSanitizeElement, currentNode, {
|
|
1400
|
-
tagName,
|
|
1401
|
-
allowedTags: ALLOWED_TAGS
|
|
1402
|
-
});
|
|
1470
|
+
const _isUnsafeNode = function _isUnsafeNode(currentNode, tagName) {
|
|
1403
1471
|
/* Detect mXSS attempts abusing namespace confusion */
|
|
1404
|
-
if (SAFE_FOR_XML && currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(
|
|
1405
|
-
_forceRemove(currentNode);
|
|
1472
|
+
if (SAFE_FOR_XML && currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.textContent) && regExpTest(ELEMENT_MARKUP_PROBE, currentNode.innerHTML)) {
|
|
1406
1473
|
return true;
|
|
1407
1474
|
}
|
|
1408
1475
|
/* Remove risky CSS construction leading to mXSS */
|
|
1409
1476
|
if (SAFE_FOR_XML && currentNode.namespaceURI === HTML_NAMESPACE && tagName === 'style' && _isNode(currentNode.firstElementChild)) {
|
|
1410
|
-
_forceRemove(currentNode);
|
|
1411
1477
|
return true;
|
|
1412
1478
|
}
|
|
1413
1479
|
/* Remove any occurrence of processing instructions */
|
|
1414
|
-
if (currentNode.nodeType === NODE_TYPE.
|
|
1415
|
-
_forceRemove(currentNode);
|
|
1480
|
+
if (currentNode.nodeType === NODE_TYPE.processingInstruction) {
|
|
1416
1481
|
return true;
|
|
1417
1482
|
}
|
|
1418
1483
|
/* Remove any kind of possibly harmful comments */
|
|
1419
|
-
if (SAFE_FOR_XML && currentNode.nodeType === NODE_TYPE.comment && regExpTest(
|
|
1420
|
-
_forceRemove(currentNode);
|
|
1484
|
+
if (SAFE_FOR_XML && currentNode.nodeType === NODE_TYPE.comment && regExpTest(COMMENT_MARKUP_PROBE, currentNode.data)) {
|
|
1421
1485
|
return true;
|
|
1422
1486
|
}
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1487
|
+
return false;
|
|
1488
|
+
};
|
|
1489
|
+
/**
|
|
1490
|
+
* Handle a node whose tag is forbidden or not allowlisted: keep
|
|
1491
|
+
* allowed custom elements (false return exits _sanitizeElements
|
|
1492
|
+
* early - namespace/fallback checks and the afterSanitizeElements
|
|
1493
|
+
* hook are intentionally skipped for kept custom elements), else
|
|
1494
|
+
* hoist content per KEEP_CONTENT and remove.
|
|
1495
|
+
*
|
|
1496
|
+
* @param currentNode the disallowed node
|
|
1497
|
+
* @param tagName the node's transformCaseFunc'd tag name
|
|
1498
|
+
* @return true if the node was removed, false if kept
|
|
1499
|
+
*/
|
|
1500
|
+
const _sanitizeDisallowedNode = function _sanitizeDisallowedNode(currentNode, tagName) {
|
|
1501
|
+
/* Check if we have a custom element to handle */
|
|
1502
|
+
if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {
|
|
1503
|
+
if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) {
|
|
1504
|
+
return false;
|
|
1433
1505
|
}
|
|
1434
|
-
|
|
1506
|
+
if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)) {
|
|
1507
|
+
return false;
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
/* Keep content except for bad-listed elements.
|
|
1435
1511
|
Use the cached prototype getters exclusively — the previous code
|
|
1436
1512
|
had `|| currentNode.parentNode` / `|| currentNode.childNodes`
|
|
1437
1513
|
fallbacks, but the cached getters always return the canonical
|
|
@@ -1439,12 +1515,12 @@
|
|
|
1439
1515
|
path was dead in safe cases and a clobbering surface in unsafe
|
|
1440
1516
|
ones. Falsy cached results stay falsy; the `if (childNodes &&
|
|
1441
1517
|
parentNode)` check already gates correctly. */
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1518
|
+
if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {
|
|
1519
|
+
const parentNode = getParentNode(currentNode);
|
|
1520
|
+
const childNodes = getChildNodes(currentNode);
|
|
1521
|
+
if (childNodes && parentNode) {
|
|
1522
|
+
const childCount = childNodes.length;
|
|
1523
|
+
/* In-place: hoist the *original* children so the iterator visits
|
|
1448
1524
|
and sanitises them through the same allowlist pass as every other
|
|
1449
1525
|
node. The caller built the tree in the live document, so the
|
|
1450
1526
|
originals carry already-queued resource events (`<img onerror>`,
|
|
@@ -1454,24 +1530,57 @@
|
|
|
1454
1530
|
root is pre-validated as an allowed tag and so is never the node
|
|
1455
1531
|
being removed, which keeps `parentNode` inside the iterator root
|
|
1456
1532
|
and the relocated child inside the serialised tree.
|
|
1457
|
-
|
|
1533
|
+
Otherwise (string / DOM-copy paths): clone. The iterator is rooted
|
|
1458
1534
|
at — and the result serialised from — `body`, so a restrictive
|
|
1459
1535
|
ALLOWED_TAGS that removes `body` itself must leave its content in
|
|
1460
1536
|
place, which only cloning does; and those paths parse into an
|
|
1461
1537
|
inert document, so their discarded originals never had a queued
|
|
1462
1538
|
event to neutralise.
|
|
1463
|
-
|
|
1539
|
+
`childNodes` is live; a tail-to-head walk keeps `childNodes[i]`
|
|
1464
1540
|
valid whether we move (drops the trailing entry) or clone (leaves
|
|
1465
1541
|
the list intact). */
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
}
|
|
1542
|
+
for (let i = childCount - 1; i >= 0; --i) {
|
|
1543
|
+
const hoisted = IN_PLACE ? childNodes[i] : cloneNode(childNodes[i], true);
|
|
1544
|
+
parentNode.insertBefore(hoisted, getNextSibling(currentNode));
|
|
1470
1545
|
}
|
|
1471
1546
|
}
|
|
1547
|
+
}
|
|
1548
|
+
_forceRemove(currentNode);
|
|
1549
|
+
return true;
|
|
1550
|
+
};
|
|
1551
|
+
/**
|
|
1552
|
+
* _sanitizeElements
|
|
1553
|
+
*
|
|
1554
|
+
* @protect nodeName
|
|
1555
|
+
* @protect textContent
|
|
1556
|
+
* @protect removeChild
|
|
1557
|
+
* @param currentNode to check for permission to exist
|
|
1558
|
+
* @return true if node was killed, false if left alive
|
|
1559
|
+
*/
|
|
1560
|
+
const _sanitizeElements = function _sanitizeElements(currentNode) {
|
|
1561
|
+
/* Execute a hook if present */
|
|
1562
|
+
_executeHooks(hooks.beforeSanitizeElements, currentNode, null);
|
|
1563
|
+
/* Check if element is clobbered or can clobber */
|
|
1564
|
+
if (_isClobbered(currentNode)) {
|
|
1565
|
+
_forceRemove(currentNode);
|
|
1566
|
+
return true;
|
|
1567
|
+
}
|
|
1568
|
+
/* Now let's check the element's type and name */
|
|
1569
|
+
const tagName = transformCaseFunc(getNodeName ? getNodeName(currentNode) : currentNode.nodeName);
|
|
1570
|
+
/* Execute a hook if present */
|
|
1571
|
+
_executeHooks(hooks.uponSanitizeElement, currentNode, {
|
|
1572
|
+
tagName,
|
|
1573
|
+
allowedTags: ALLOWED_TAGS
|
|
1574
|
+
});
|
|
1575
|
+
/* Remove mXSS vectors, processing instructions and risky comments */
|
|
1576
|
+
if (_isUnsafeNode(currentNode, tagName)) {
|
|
1472
1577
|
_forceRemove(currentNode);
|
|
1473
1578
|
return true;
|
|
1474
1579
|
}
|
|
1580
|
+
/* Remove element if anything forbids its presence */
|
|
1581
|
+
if (FORBID_TAGS[tagName] || !(EXTRA_ELEMENT_HANDLING.tagCheck instanceof Function && EXTRA_ELEMENT_HANDLING.tagCheck(tagName)) && !ALLOWED_TAGS[tagName]) {
|
|
1582
|
+
return _sanitizeDisallowedNode(currentNode, tagName);
|
|
1583
|
+
}
|
|
1475
1584
|
/* Check whether element has a valid namespace.
|
|
1476
1585
|
Realm-safe check (GHSA-hpcv-96wg-7vj8): use the cached Node.prototype
|
|
1477
1586
|
nodeType getter rather than `instanceof Element`, which is realm-
|
|
@@ -1484,17 +1593,14 @@
|
|
|
1484
1593
|
return true;
|
|
1485
1594
|
}
|
|
1486
1595
|
/* Make sure that older browsers don't get fallback-tag mXSS */
|
|
1487
|
-
if ((tagName === 'noscript' || tagName === 'noembed' || tagName === 'noframes') && regExpTest(
|
|
1596
|
+
if ((tagName === 'noscript' || tagName === 'noembed' || tagName === 'noframes') && regExpTest(FALLBACK_TAG_CLOSE, currentNode.innerHTML)) {
|
|
1488
1597
|
_forceRemove(currentNode);
|
|
1489
1598
|
return true;
|
|
1490
1599
|
}
|
|
1491
1600
|
/* Sanitize element content to be template-safe */
|
|
1492
1601
|
if (SAFE_FOR_TEMPLATES && currentNode.nodeType === NODE_TYPE.text) {
|
|
1493
1602
|
/* Get the element's text content */
|
|
1494
|
-
content = currentNode.textContent;
|
|
1495
|
-
arrayForEach([MUSTACHE_EXPR$1, ERB_EXPR$1, TMPLIT_EXPR$1], expr => {
|
|
1496
|
-
content = stringReplace(content, expr, ' ');
|
|
1497
|
-
});
|
|
1603
|
+
const content = _stripTemplateExpressions(currentNode.textContent);
|
|
1498
1604
|
if (currentNode.textContent !== content) {
|
|
1499
1605
|
arrayPush(DOMPurify.removed, {
|
|
1500
1606
|
element: currentNode.cloneNode()
|
|
@@ -1529,7 +1635,7 @@
|
|
|
1529
1635
|
(https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes)
|
|
1530
1636
|
XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804)
|
|
1531
1637
|
We don't need to check the value; it's always URI safe. */
|
|
1532
|
-
if (ALLOW_DATA_ATTR &&
|
|
1638
|
+
if (ALLOW_DATA_ATTR && regExpTest(DATA_ATTR$1, lcName)) ; else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR$1, lcName)) ; else if (!nameIsPermitted) {
|
|
1533
1639
|
if (
|
|
1534
1640
|
// First condition does a very basic check if a) it's basically a valid custom element tagname AND
|
|
1535
1641
|
// b) if the tagName passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck
|
|
@@ -1561,6 +1667,63 @@
|
|
|
1561
1667
|
const _isBasicCustomElement = function _isBasicCustomElement(tagName) {
|
|
1562
1668
|
return !RESERVED_CUSTOM_ELEMENT_NAMES[stringToLowerCase(tagName)] && regExpTest(CUSTOM_ELEMENT$1, tagName);
|
|
1563
1669
|
};
|
|
1670
|
+
/**
|
|
1671
|
+
* Wrap an attribute value in the matching Trusted Types object when
|
|
1672
|
+
* the active policy requires it. Namespaced attributes pass through
|
|
1673
|
+
* unchanged (no TT support yet, see
|
|
1674
|
+
* https://bugs.chromium.org/p/chromium/issues/detail?id=1305293).
|
|
1675
|
+
*
|
|
1676
|
+
* @param lcTag lowercase tag name of the containing element
|
|
1677
|
+
* @param lcName lowercase attribute name
|
|
1678
|
+
* @param namespaceURI the attribute's namespace, if any
|
|
1679
|
+
* @param value the attribute value to wrap
|
|
1680
|
+
* @return the value, wrapped when Trusted Types demand it
|
|
1681
|
+
*/
|
|
1682
|
+
const _applyTrustedTypesToAttribute = function _applyTrustedTypesToAttribute(lcTag, lcName, namespaceURI, value) {
|
|
1683
|
+
if (trustedTypesPolicy && typeof trustedTypes === 'object' && typeof trustedTypes.getAttributeType === 'function' && !namespaceURI) {
|
|
1684
|
+
switch (trustedTypes.getAttributeType(lcTag, lcName)) {
|
|
1685
|
+
case 'TrustedHTML':
|
|
1686
|
+
{
|
|
1687
|
+
return _createTrustedHTML(value);
|
|
1688
|
+
}
|
|
1689
|
+
case 'TrustedScriptURL':
|
|
1690
|
+
{
|
|
1691
|
+
return _createTrustedScriptURL(value);
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
return value;
|
|
1696
|
+
};
|
|
1697
|
+
/**
|
|
1698
|
+
* Write a modified attribute value back onto the element. On
|
|
1699
|
+
* success, re-probe for clobbering introduced by the new value and
|
|
1700
|
+
* remove the element when found; otherwise pop the removal entry
|
|
1701
|
+
* recorded by the earlier _removeAttribute (long-standing pairing
|
|
1702
|
+
* with the SANITIZE_NAMED_PROPS path - do not "fix" casually). On
|
|
1703
|
+
* failure, remove the attribute instead.
|
|
1704
|
+
*
|
|
1705
|
+
* @param currentNode the element carrying the attribute
|
|
1706
|
+
* @param name the attribute name as present on the element
|
|
1707
|
+
* @param namespaceURI the attribute's namespace, if any
|
|
1708
|
+
* @param value the new attribute value
|
|
1709
|
+
*/
|
|
1710
|
+
const _setAttributeValue = function _setAttributeValue(currentNode, name, namespaceURI, value) {
|
|
1711
|
+
try {
|
|
1712
|
+
if (namespaceURI) {
|
|
1713
|
+
currentNode.setAttributeNS(namespaceURI, name, value);
|
|
1714
|
+
} else {
|
|
1715
|
+
/* Fallback to setAttribute() for browser-unrecognized namespaces e.g. "x-schema". */
|
|
1716
|
+
currentNode.setAttribute(name, value);
|
|
1717
|
+
}
|
|
1718
|
+
if (_isClobbered(currentNode)) {
|
|
1719
|
+
_forceRemove(currentNode);
|
|
1720
|
+
} else {
|
|
1721
|
+
arrayPop(DOMPurify.removed);
|
|
1722
|
+
}
|
|
1723
|
+
} catch (_) {
|
|
1724
|
+
_removeAttribute(name, currentNode);
|
|
1725
|
+
}
|
|
1726
|
+
};
|
|
1564
1727
|
/**
|
|
1565
1728
|
* _sanitizeAttributes
|
|
1566
1729
|
*
|
|
@@ -1587,6 +1750,7 @@
|
|
|
1587
1750
|
forceKeepAttr: undefined
|
|
1588
1751
|
};
|
|
1589
1752
|
let l = attributes.length;
|
|
1753
|
+
const lcTag = transformCaseFunc(currentNode.nodeName);
|
|
1590
1754
|
/* Go backwards over all attributes; safely remove bad ones */
|
|
1591
1755
|
while (l--) {
|
|
1592
1756
|
const attr = attributes[l];
|
|
@@ -1624,7 +1788,7 @@
|
|
|
1624
1788
|
_removeAttribute(name, currentNode);
|
|
1625
1789
|
continue;
|
|
1626
1790
|
}
|
|
1627
|
-
/* Did the hooks
|
|
1791
|
+
/* Did the hooks force-keep the attribute? */
|
|
1628
1792
|
if (hookEvent.forceKeepAttr) {
|
|
1629
1793
|
continue;
|
|
1630
1794
|
}
|
|
@@ -1634,56 +1798,24 @@
|
|
|
1634
1798
|
continue;
|
|
1635
1799
|
}
|
|
1636
1800
|
/* Work around a security issue in jQuery 3.0 */
|
|
1637
|
-
if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(
|
|
1801
|
+
if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(SELF_CLOSING_TAG, value)) {
|
|
1638
1802
|
_removeAttribute(name, currentNode);
|
|
1639
1803
|
continue;
|
|
1640
1804
|
}
|
|
1641
1805
|
/* Sanitize attribute content to be template-safe */
|
|
1642
1806
|
if (SAFE_FOR_TEMPLATES) {
|
|
1643
|
-
|
|
1644
|
-
value = stringReplace(value, expr, ' ');
|
|
1645
|
-
});
|
|
1807
|
+
value = _stripTemplateExpressions(value);
|
|
1646
1808
|
}
|
|
1647
1809
|
/* Is `value` valid for this attribute? */
|
|
1648
|
-
const lcTag = transformCaseFunc(currentNode.nodeName);
|
|
1649
1810
|
if (!_isValidAttribute(lcTag, lcName, value)) {
|
|
1650
1811
|
_removeAttribute(name, currentNode);
|
|
1651
1812
|
continue;
|
|
1652
1813
|
}
|
|
1653
1814
|
/* Handle attributes that require Trusted Types */
|
|
1654
|
-
|
|
1655
|
-
if (namespaceURI) ; else {
|
|
1656
|
-
switch (trustedTypes.getAttributeType(lcTag, lcName)) {
|
|
1657
|
-
case 'TrustedHTML':
|
|
1658
|
-
{
|
|
1659
|
-
value = _createTrustedHTML(value);
|
|
1660
|
-
break;
|
|
1661
|
-
}
|
|
1662
|
-
case 'TrustedScriptURL':
|
|
1663
|
-
{
|
|
1664
|
-
value = _createTrustedScriptURL(value);
|
|
1665
|
-
break;
|
|
1666
|
-
}
|
|
1667
|
-
}
|
|
1668
|
-
}
|
|
1669
|
-
}
|
|
1815
|
+
value = _applyTrustedTypesToAttribute(lcTag, lcName, namespaceURI, value);
|
|
1670
1816
|
/* Handle invalid data-* attribute set by try-catching it */
|
|
1671
1817
|
if (value !== initValue) {
|
|
1672
|
-
|
|
1673
|
-
if (namespaceURI) {
|
|
1674
|
-
currentNode.setAttributeNS(namespaceURI, name, value);
|
|
1675
|
-
} else {
|
|
1676
|
-
/* Fallback to setAttribute() for browser-unrecognized namespaces e.g. "x-schema". */
|
|
1677
|
-
currentNode.setAttribute(name, value);
|
|
1678
|
-
}
|
|
1679
|
-
if (_isClobbered(currentNode)) {
|
|
1680
|
-
_forceRemove(currentNode);
|
|
1681
|
-
} else {
|
|
1682
|
-
arrayPop(DOMPurify.removed);
|
|
1683
|
-
}
|
|
1684
|
-
} catch (_) {
|
|
1685
|
-
_removeAttribute(name, currentNode);
|
|
1686
|
-
}
|
|
1818
|
+
_setAttributeValue(currentNode, name, namespaceURI, value);
|
|
1687
1819
|
}
|
|
1688
1820
|
}
|
|
1689
1821
|
/* Execute a hook if present */
|
|
@@ -1725,7 +1857,7 @@
|
|
|
1725
1857
|
iterator also surfaces. */
|
|
1726
1858
|
const shadowNodeType = getNodeType ? getNodeType(shadowNode) : shadowNode.nodeType;
|
|
1727
1859
|
if (shadowNodeType === NODE_TYPE.element) {
|
|
1728
|
-
const innerSr = getShadowRoot
|
|
1860
|
+
const innerSr = getShadowRoot(shadowNode);
|
|
1729
1861
|
if (_isDocumentFragment(innerSr)) {
|
|
1730
1862
|
_sanitizeAttachedShadowRoots(innerSr);
|
|
1731
1863
|
_sanitizeShadowDOM2(innerSr);
|
|
@@ -1787,7 +1919,7 @@
|
|
|
1787
1919
|
/* (pushed last → processed first) Children, snapshotted in reverse so
|
|
1788
1920
|
the first child is processed first. Snapshotting matters because a
|
|
1789
1921
|
hook may detach siblings mid-walk. */
|
|
1790
|
-
const childNodes = getChildNodes
|
|
1922
|
+
const childNodes = getChildNodes(node);
|
|
1791
1923
|
if (childNodes) {
|
|
1792
1924
|
for (let i = childNodes.length - 1; i >= 0; --i) {
|
|
1793
1925
|
stack.push({
|
|
@@ -1817,7 +1949,7 @@
|
|
|
1817
1949
|
silently skipped foreign-realm shadow roots (e.g.
|
|
1818
1950
|
iframe.contentDocument attachShadow). */
|
|
1819
1951
|
if (isElement) {
|
|
1820
|
-
const sr = getShadowRoot
|
|
1952
|
+
const sr = getShadowRoot(node);
|
|
1821
1953
|
if (_isDocumentFragment(sr)) {
|
|
1822
1954
|
/* Push the deferred sanitise first so it pops after the shadow
|
|
1823
1955
|
walk we push next, i.e. nested shadow roots are discovered
|
|
@@ -1859,9 +1991,31 @@
|
|
|
1859
1991
|
return dirty;
|
|
1860
1992
|
}
|
|
1861
1993
|
/* Assign config vars */
|
|
1862
|
-
if (
|
|
1994
|
+
if (SET_CONFIG) {
|
|
1995
|
+
/* Persistent setConfig() path: _parseConfig is skipped, so the sets are
|
|
1996
|
+
* not re-derived per call. Restore them from the pristine bindings
|
|
1997
|
+
* captured at setConfig() time so a previous call's hook clone (mutated
|
|
1998
|
+
* below) does not carry over. */
|
|
1999
|
+
ALLOWED_TAGS = SET_CONFIG_ALLOWED_TAGS;
|
|
2000
|
+
ALLOWED_ATTR = SET_CONFIG_ALLOWED_ATTR;
|
|
2001
|
+
} else {
|
|
1863
2002
|
_parseConfig(cfg);
|
|
1864
2003
|
}
|
|
2004
|
+
/* Clone the hook-mutable allowlists before the walk whenever an
|
|
2005
|
+
* uponSanitize* hook is registered. The hook event exposes ALLOWED_TAGS
|
|
2006
|
+
* and ALLOWED_ATTR by reference (as allowedTags / allowedAttributes), so
|
|
2007
|
+
* a hook that widens them would otherwise mutate the shared set
|
|
2008
|
+
* permanently: across later calls and across every element. Cloning per
|
|
2009
|
+
* walk keeps documented in-call widening working while scoping it to the
|
|
2010
|
+
* call. A single guard for both config paths - the per-call path rebinds
|
|
2011
|
+
* the sets in _parseConfig each call, the persistent path restores them
|
|
2012
|
+
* from the captured bindings just above - so the two cannot diverge. */
|
|
2013
|
+
if (hooks.uponSanitizeElement.length > 0 || hooks.uponSanitizeAttribute.length > 0) {
|
|
2014
|
+
ALLOWED_TAGS = clone(ALLOWED_TAGS);
|
|
2015
|
+
}
|
|
2016
|
+
if (hooks.uponSanitizeAttribute.length > 0) {
|
|
2017
|
+
ALLOWED_ATTR = clone(ALLOWED_ATTR);
|
|
2018
|
+
}
|
|
1865
2019
|
/* Clean up removed elements */
|
|
1866
2020
|
DOMPurify.removed = [];
|
|
1867
2021
|
/* Resolve IN_PLACE for this call without mutating persistent config.
|
|
@@ -2029,9 +2183,7 @@
|
|
|
2029
2183
|
}
|
|
2030
2184
|
/* Sanitize final string template-safe */
|
|
2031
2185
|
if (SAFE_FOR_TEMPLATES) {
|
|
2032
|
-
|
|
2033
|
-
serializedHTML = stringReplace(serializedHTML, expr, ' ');
|
|
2034
|
-
});
|
|
2186
|
+
serializedHTML = _stripTemplateExpressions(serializedHTML);
|
|
2035
2187
|
}
|
|
2036
2188
|
return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? _createTrustedHTML(serializedHTML) : serializedHTML;
|
|
2037
2189
|
};
|
|
@@ -2039,10 +2191,14 @@
|
|
|
2039
2191
|
let cfg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
|
2040
2192
|
_parseConfig(cfg);
|
|
2041
2193
|
SET_CONFIG = true;
|
|
2194
|
+
SET_CONFIG_ALLOWED_TAGS = ALLOWED_TAGS;
|
|
2195
|
+
SET_CONFIG_ALLOWED_ATTR = ALLOWED_ATTR;
|
|
2042
2196
|
};
|
|
2043
2197
|
DOMPurify.clearConfig = function () {
|
|
2044
2198
|
CONFIG = null;
|
|
2045
2199
|
SET_CONFIG = false;
|
|
2200
|
+
SET_CONFIG_ALLOWED_TAGS = null;
|
|
2201
|
+
SET_CONFIG_ALLOWED_ATTR = null;
|
|
2046
2202
|
// Drop any caller-supplied Trusted Types policy so it cannot poison later
|
|
2047
2203
|
// `RETURN_TRUSTED_TYPE` output. The internal default policy (cached, and
|
|
2048
2204
|
// never recreated — Trusted Types throws on duplicate names) is restored by
|
|
@@ -2063,9 +2219,19 @@
|
|
|
2063
2219
|
if (typeof hookFunction !== 'function') {
|
|
2064
2220
|
return;
|
|
2065
2221
|
}
|
|
2222
|
+
/* Reject unknown entry points. Without this, a non-hook key (e.g.
|
|
2223
|
+
* '__proto__') indexes off the prototype chain rather than a real
|
|
2224
|
+
* hook array, and arrayPush then writes to Object.prototype. Guard
|
|
2225
|
+
* with an own-property check against the known hook names. */
|
|
2226
|
+
if (!objectHasOwnProperty(hooks, entryPoint)) {
|
|
2227
|
+
return;
|
|
2228
|
+
}
|
|
2066
2229
|
arrayPush(hooks[entryPoint], hookFunction);
|
|
2067
2230
|
};
|
|
2068
2231
|
DOMPurify.removeHook = function (entryPoint, hookFunction) {
|
|
2232
|
+
if (!objectHasOwnProperty(hooks, entryPoint)) {
|
|
2233
|
+
return undefined;
|
|
2234
|
+
}
|
|
2069
2235
|
if (hookFunction !== undefined) {
|
|
2070
2236
|
const index = arrayLastIndexOf(hooks[entryPoint], hookFunction);
|
|
2071
2237
|
return index === -1 ? undefined : arraySplice(hooks[entryPoint], index, 1)[0];
|
|
@@ -2073,6 +2239,9 @@
|
|
|
2073
2239
|
return arrayPop(hooks[entryPoint]);
|
|
2074
2240
|
};
|
|
2075
2241
|
DOMPurify.removeHooks = function (entryPoint) {
|
|
2242
|
+
if (!objectHasOwnProperty(hooks, entryPoint)) {
|
|
2243
|
+
return;
|
|
2244
|
+
}
|
|
2076
2245
|
hooks[entryPoint] = [];
|
|
2077
2246
|
};
|
|
2078
2247
|
DOMPurify.removeAllHooks = function () {
|