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