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