dompurify 3.4.8 → 3.4.10
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 +21 -12
- package/dist/purify.cjs.d.ts +3 -3
- package/dist/purify.cjs.js +677 -245
- package/dist/purify.cjs.js.map +1 -1
- package/dist/purify.cov.cjs.js +26330 -0
- package/dist/purify.cov.cjs.js.map +1 -0
- package/dist/purify.es.d.mts +3 -3
- package/dist/purify.es.mjs +677 -245
- package/dist/purify.es.mjs.map +1 -1
- package/dist/purify.js +677 -245
- 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 +15 -6
- package/src/purify.ts +835 -709
- package/src/regexp.ts +8 -0
- package/src/types.ts +356 -0
package/src/purify.ts
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/indent */
|
|
2
2
|
|
|
3
|
-
import type {
|
|
4
|
-
TrustedHTML,
|
|
5
|
-
TrustedTypesWindow,
|
|
6
|
-
} from 'trusted-types/lib/index.js';
|
|
7
3
|
import type { Config, UseProfilesConfig } from './config';
|
|
4
|
+
import type { DOMPurify, HooksMap, HookFunction, WindowLike } from './types';
|
|
8
5
|
import * as TAGS from './tags.js';
|
|
9
6
|
import * as ATTRS from './attrs.js';
|
|
10
7
|
import * as EXPRESSIONS from './regexp.js';
|
|
@@ -13,6 +10,7 @@ import {
|
|
|
13
10
|
clone,
|
|
14
11
|
entries,
|
|
15
12
|
freeze,
|
|
13
|
+
seal,
|
|
16
14
|
arrayForEach,
|
|
17
15
|
arrayIsArray,
|
|
18
16
|
arrayLastIndexOf,
|
|
@@ -36,6 +34,21 @@ import {
|
|
|
36
34
|
|
|
37
35
|
export type { Config } from './config';
|
|
38
36
|
|
|
37
|
+
export type {
|
|
38
|
+
DOMPurify,
|
|
39
|
+
RemovedElement,
|
|
40
|
+
RemovedAttribute,
|
|
41
|
+
HookName,
|
|
42
|
+
NodeHook,
|
|
43
|
+
ElementHook,
|
|
44
|
+
DocumentFragmentHook,
|
|
45
|
+
UponSanitizeElementHook,
|
|
46
|
+
UponSanitizeAttributeHook,
|
|
47
|
+
UponSanitizeElementHookEvent,
|
|
48
|
+
UponSanitizeAttributeHookEvent,
|
|
49
|
+
WindowLike,
|
|
50
|
+
} from './types';
|
|
51
|
+
|
|
39
52
|
declare const VERSION: string;
|
|
40
53
|
|
|
41
54
|
// https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
|
|
@@ -46,7 +59,7 @@ const NODE_TYPE = {
|
|
|
46
59
|
cdataSection: 4,
|
|
47
60
|
entityReference: 5, // Deprecated
|
|
48
61
|
entityNode: 6, // Deprecated
|
|
49
|
-
|
|
62
|
+
processingInstruction: 7,
|
|
50
63
|
comment: 8,
|
|
51
64
|
document: 9,
|
|
52
65
|
documentType: 10,
|
|
@@ -122,6 +135,36 @@ const _createHooksMap = function (): HooksMap {
|
|
|
122
135
|
};
|
|
123
136
|
};
|
|
124
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Resolve a set-valued configuration option: a fresh set built from
|
|
140
|
+
* cfg[key] when it is an own array property (seeded with a clone of
|
|
141
|
+
* options.base when given, case-normalized via options.transform),
|
|
142
|
+
* the fallback set otherwise.
|
|
143
|
+
*
|
|
144
|
+
* @param cfg the cloned, prototype-free configuration object
|
|
145
|
+
* @param key the configuration property to read
|
|
146
|
+
* @param fallback the set to use when the option is absent or not an array
|
|
147
|
+
* @param options transform and optional base set to merge into
|
|
148
|
+
* @returns the resolved set
|
|
149
|
+
*/
|
|
150
|
+
const _resolveSetOption = function (
|
|
151
|
+
cfg: Config,
|
|
152
|
+
key: keyof Config,
|
|
153
|
+
fallback: Record<string, boolean>,
|
|
154
|
+
options: {
|
|
155
|
+
transform: Parameters<typeof addToSet>[2];
|
|
156
|
+
base?: Record<string, boolean>;
|
|
157
|
+
}
|
|
158
|
+
): Record<string, boolean> {
|
|
159
|
+
return objectHasOwnProperty(cfg, key) && arrayIsArray(cfg[key])
|
|
160
|
+
? addToSet(
|
|
161
|
+
options.base ? clone(options.base) : {},
|
|
162
|
+
cfg[key] as readonly unknown[],
|
|
163
|
+
options.transform
|
|
164
|
+
)
|
|
165
|
+
: fallback;
|
|
166
|
+
};
|
|
167
|
+
|
|
125
168
|
function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
126
169
|
const DOMPurify: DOMPurify = (root: WindowLike) => createDOMPurify(root);
|
|
127
170
|
|
|
@@ -189,30 +232,71 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
189
232
|
let trustedTypesPolicy;
|
|
190
233
|
let emptyHTML = '';
|
|
191
234
|
|
|
235
|
+
// The instance's own internal Trusted Types policy. Unlike a caller-supplied
|
|
236
|
+
// `TRUSTED_TYPES_POLICY`, this is created at most once — Trusted Types throws
|
|
237
|
+
// on duplicate policy names — and is the only policy allowed to persist
|
|
238
|
+
// across configurations and survive `clearConfig()`.
|
|
239
|
+
let defaultTrustedTypesPolicy;
|
|
240
|
+
let defaultTrustedTypesPolicyResolved = false;
|
|
241
|
+
|
|
192
242
|
// Tracks whether we are already inside a call to the configured Trusted Types
|
|
193
|
-
// policy
|
|
243
|
+
// policy (`createHTML` or `createScriptURL`). If a supplied policy callback
|
|
194
244
|
// itself calls `DOMPurify.sanitize` (the cause of #1422), `sanitize` would
|
|
195
245
|
// re-enter the policy and recurse until the stack overflows. We detect that
|
|
196
|
-
// re-entry and throw a clear, actionable error instead.
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
246
|
+
// re-entry and throw a clear, actionable error instead. The guard is shared
|
|
247
|
+
// across both callbacks, because either one re-entering `sanitize` triggers
|
|
248
|
+
// the same unbounded recursion.
|
|
249
|
+
let IN_TRUSTED_TYPES_POLICY = 0;
|
|
250
|
+
const _assertNotInTrustedTypesPolicy = function (): void {
|
|
251
|
+
if (IN_TRUSTED_TYPES_POLICY > 0) {
|
|
200
252
|
throw typeErrorCreate(
|
|
201
|
-
'
|
|
202
|
-
'DOMPurify.sanitize, as that causes
|
|
203
|
-
'a policy whose
|
|
204
|
-
'see the "DOMPurify and Trusted
|
|
253
|
+
'A configured TRUSTED_TYPES_POLICY callback (createHTML or ' +
|
|
254
|
+
'createScriptURL) must not call DOMPurify.sanitize, as that causes ' +
|
|
255
|
+
'infinite recursion. Do not pass a policy whose callbacks wrap ' +
|
|
256
|
+
'DOMPurify as TRUSTED_TYPES_POLICY; see the "DOMPurify and Trusted ' +
|
|
257
|
+
'Types" section of the README.'
|
|
205
258
|
);
|
|
206
259
|
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const _createTrustedHTML = function (html: string): string {
|
|
263
|
+
_assertNotInTrustedTypesPolicy();
|
|
207
264
|
|
|
208
|
-
|
|
265
|
+
IN_TRUSTED_TYPES_POLICY++;
|
|
209
266
|
try {
|
|
210
267
|
return trustedTypesPolicy.createHTML(html);
|
|
211
268
|
} finally {
|
|
212
|
-
|
|
269
|
+
IN_TRUSTED_TYPES_POLICY--;
|
|
213
270
|
}
|
|
214
271
|
};
|
|
215
272
|
|
|
273
|
+
const _createTrustedScriptURL = function (scriptUrl: string): string {
|
|
274
|
+
_assertNotInTrustedTypesPolicy();
|
|
275
|
+
|
|
276
|
+
IN_TRUSTED_TYPES_POLICY++;
|
|
277
|
+
try {
|
|
278
|
+
return trustedTypesPolicy.createScriptURL(scriptUrl);
|
|
279
|
+
} finally {
|
|
280
|
+
IN_TRUSTED_TYPES_POLICY--;
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
// Lazily resolve (and cache) the instance's internal default policy.
|
|
285
|
+
// Resolution is attempted at most once: a successful `createPolicy` cannot be
|
|
286
|
+
// repeated (Trusted Types throws on duplicate names), and a failed or
|
|
287
|
+
// unsupported attempt must not be retried on every parse.
|
|
288
|
+
const _getDefaultTrustedTypesPolicy = function () {
|
|
289
|
+
if (!defaultTrustedTypesPolicyResolved) {
|
|
290
|
+
defaultTrustedTypesPolicy = _createTrustedTypesPolicy(
|
|
291
|
+
trustedTypes,
|
|
292
|
+
currentScript
|
|
293
|
+
);
|
|
294
|
+
defaultTrustedTypesPolicyResolved = true;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return defaultTrustedTypesPolicy;
|
|
298
|
+
};
|
|
299
|
+
|
|
216
300
|
const {
|
|
217
301
|
implementation,
|
|
218
302
|
createNodeIterator,
|
|
@@ -421,6 +505,16 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
421
505
|
'noscript',
|
|
422
506
|
'plaintext',
|
|
423
507
|
'script',
|
|
508
|
+
// <selectedcontent> mirrors the selected <option>'s subtree, cloned by
|
|
509
|
+
// the UA (customizable <select>) — including any on* handlers — and the
|
|
510
|
+
// engine re-mirrors synchronously whenever a removal changes which
|
|
511
|
+
// option/selectedcontent is current, even inside DOMPurify's inert
|
|
512
|
+
// DOMParser document. Hoisting its children on removal re-inserts a fresh
|
|
513
|
+
// mirror target ahead of the walk, which the engine refills, looping
|
|
514
|
+
// forever (DoS) and amplifying output. Dropping its content on removal
|
|
515
|
+
// (rather than hoisting) breaks that cascade; the content is a duplicate
|
|
516
|
+
// of the option, which is sanitized on its own. See campaign-3 F1/F6.
|
|
517
|
+
'selectedcontent',
|
|
424
518
|
'style',
|
|
425
519
|
'svg',
|
|
426
520
|
'template',
|
|
@@ -475,15 +569,20 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
475
569
|
stringToString
|
|
476
570
|
);
|
|
477
571
|
|
|
478
|
-
|
|
572
|
+
const DEFAULT_MATHML_TEXT_INTEGRATION_POINTS = freeze([
|
|
479
573
|
'mi',
|
|
480
574
|
'mo',
|
|
481
575
|
'mn',
|
|
482
576
|
'ms',
|
|
483
577
|
'mtext',
|
|
484
578
|
]);
|
|
579
|
+
let MATHML_TEXT_INTEGRATION_POINTS = addToSet(
|
|
580
|
+
{},
|
|
581
|
+
DEFAULT_MATHML_TEXT_INTEGRATION_POINTS
|
|
582
|
+
);
|
|
485
583
|
|
|
486
|
-
|
|
584
|
+
const DEFAULT_HTML_INTEGRATION_POINTS = freeze(['annotation-xml']);
|
|
585
|
+
let HTML_INTEGRATION_POINTS = addToSet({}, DEFAULT_HTML_INTEGRATION_POINTS);
|
|
487
586
|
|
|
488
587
|
// Certain elements are allowed in both SVG and HTML
|
|
489
588
|
// namespace. We need to specify them explicitly
|
|
@@ -549,52 +648,48 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
549
648
|
: stringToLowerCase;
|
|
550
649
|
|
|
551
650
|
/* Set configuration parameters */
|
|
552
|
-
ALLOWED_TAGS =
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
DATA_URI_TAGS =
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
FORBID_ATTR =
|
|
595
|
-
objectHasOwnProperty(cfg, 'FORBID_ATTR') && arrayIsArray(cfg.FORBID_ATTR)
|
|
596
|
-
? addToSet({}, cfg.FORBID_ATTR, transformCaseFunc)
|
|
597
|
-
: clone({});
|
|
651
|
+
ALLOWED_TAGS = _resolveSetOption(
|
|
652
|
+
cfg,
|
|
653
|
+
'ALLOWED_TAGS',
|
|
654
|
+
DEFAULT_ALLOWED_TAGS,
|
|
655
|
+
{ transform: transformCaseFunc }
|
|
656
|
+
);
|
|
657
|
+
ALLOWED_ATTR = _resolveSetOption(
|
|
658
|
+
cfg,
|
|
659
|
+
'ALLOWED_ATTR',
|
|
660
|
+
DEFAULT_ALLOWED_ATTR,
|
|
661
|
+
{ transform: transformCaseFunc }
|
|
662
|
+
);
|
|
663
|
+
ALLOWED_NAMESPACES = _resolveSetOption(
|
|
664
|
+
cfg,
|
|
665
|
+
'ALLOWED_NAMESPACES',
|
|
666
|
+
DEFAULT_ALLOWED_NAMESPACES,
|
|
667
|
+
{ transform: stringToString }
|
|
668
|
+
);
|
|
669
|
+
URI_SAFE_ATTRIBUTES = _resolveSetOption(
|
|
670
|
+
cfg,
|
|
671
|
+
'ADD_URI_SAFE_ATTR',
|
|
672
|
+
DEFAULT_URI_SAFE_ATTRIBUTES,
|
|
673
|
+
{ transform: transformCaseFunc, base: DEFAULT_URI_SAFE_ATTRIBUTES }
|
|
674
|
+
);
|
|
675
|
+
DATA_URI_TAGS = _resolveSetOption(
|
|
676
|
+
cfg,
|
|
677
|
+
'ADD_DATA_URI_TAGS',
|
|
678
|
+
DEFAULT_DATA_URI_TAGS,
|
|
679
|
+
{ transform: transformCaseFunc, base: DEFAULT_DATA_URI_TAGS }
|
|
680
|
+
);
|
|
681
|
+
FORBID_CONTENTS = _resolveSetOption(
|
|
682
|
+
cfg,
|
|
683
|
+
'FORBID_CONTENTS',
|
|
684
|
+
DEFAULT_FORBID_CONTENTS,
|
|
685
|
+
{ transform: transformCaseFunc }
|
|
686
|
+
);
|
|
687
|
+
FORBID_TAGS = _resolveSetOption(cfg, 'FORBID_TAGS', clone({}), {
|
|
688
|
+
transform: transformCaseFunc,
|
|
689
|
+
});
|
|
690
|
+
FORBID_ATTR = _resolveSetOption(cfg, 'FORBID_ATTR', clone({}), {
|
|
691
|
+
transform: transformCaseFunc,
|
|
692
|
+
});
|
|
598
693
|
USE_PROFILES = objectHasOwnProperty(cfg, 'USE_PROFILES')
|
|
599
694
|
? cfg.USE_PROFILES && typeof cfg.USE_PROFILES === 'object'
|
|
600
695
|
? clone(cfg.USE_PROFILES)
|
|
@@ -628,14 +723,14 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
628
723
|
cfg.MATHML_TEXT_INTEGRATION_POINTS &&
|
|
629
724
|
typeof cfg.MATHML_TEXT_INTEGRATION_POINTS === 'object'
|
|
630
725
|
? clone(cfg.MATHML_TEXT_INTEGRATION_POINTS)
|
|
631
|
-
: addToSet({},
|
|
726
|
+
: addToSet({}, DEFAULT_MATHML_TEXT_INTEGRATION_POINTS); // Default built-in map
|
|
632
727
|
|
|
633
728
|
HTML_INTEGRATION_POINTS =
|
|
634
729
|
objectHasOwnProperty(cfg, 'HTML_INTEGRATION_POINTS') &&
|
|
635
730
|
cfg.HTML_INTEGRATION_POINTS &&
|
|
636
731
|
typeof cfg.HTML_INTEGRATION_POINTS === 'object'
|
|
637
732
|
? clone(cfg.HTML_INTEGRATION_POINTS)
|
|
638
|
-
: addToSet({},
|
|
733
|
+
: addToSet({}, DEFAULT_HTML_INTEGRATION_POINTS); // Default built-in map
|
|
639
734
|
|
|
640
735
|
const customElementHandling =
|
|
641
736
|
objectHasOwnProperty(cfg, 'CUSTOM_ELEMENT_HANDLING') &&
|
|
@@ -672,6 +767,8 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
672
767
|
customElementHandling.allowCustomizedBuiltInElements; // Default undefined
|
|
673
768
|
}
|
|
674
769
|
|
|
770
|
+
seal(CUSTOM_ELEMENT_HANDLING);
|
|
771
|
+
|
|
675
772
|
if (SAFE_FOR_TEMPLATES) {
|
|
676
773
|
ALLOW_DATA_ATTR = false;
|
|
677
774
|
}
|
|
@@ -783,6 +880,13 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
783
880
|
delete FORBID_TAGS.tbody;
|
|
784
881
|
}
|
|
785
882
|
|
|
883
|
+
// Re-derive the active Trusted Types policy from this configuration on
|
|
884
|
+
// every parse. The active policy must never be sticky closure state that
|
|
885
|
+
// outlives the config that set it: a caller-supplied policy left in place
|
|
886
|
+
// after `clearConfig()` — or after a later call that supplied none, or
|
|
887
|
+
// `TRUSTED_TYPES_POLICY: null` — could sign a subsequent "default"
|
|
888
|
+
// `RETURN_TRUSTED_TYPE` result with a foreign, possibly unsafe policy.
|
|
889
|
+
// See GHSA-vxr8-fq34-vvx9.
|
|
786
890
|
if (cfg.TRUSTED_TYPES_POLICY) {
|
|
787
891
|
if (typeof cfg.TRUSTED_TYPES_POLICY.createHTML !== 'function') {
|
|
788
892
|
throw typeErrorCreate(
|
|
@@ -796,7 +900,7 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
796
900
|
);
|
|
797
901
|
}
|
|
798
902
|
|
|
799
|
-
//
|
|
903
|
+
// A caller-supplied policy applies to this configuration only.
|
|
800
904
|
const previousTrustedTypesPolicy = trustedTypesPolicy;
|
|
801
905
|
trustedTypesPolicy = cfg.TRUSTED_TYPES_POLICY;
|
|
802
906
|
|
|
@@ -810,23 +914,31 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
810
914
|
trustedTypesPolicy = previousTrustedTypesPolicy;
|
|
811
915
|
throw error;
|
|
812
916
|
}
|
|
917
|
+
} else if (cfg.TRUSTED_TYPES_POLICY === null) {
|
|
918
|
+
// Explicit opt-out for this call: perform no Trusted Types signing and
|
|
919
|
+
// create nothing (so a strict `trusted-types` CSP that disallows a
|
|
920
|
+
// `dompurify` policy can still call `sanitize` from inside its own
|
|
921
|
+
// policy — see #1422). Resetting to `undefined` rather than a sticky
|
|
922
|
+
// `null` also drops any previously retained caller policy, so it cannot
|
|
923
|
+
// resurface on a later call, while still allowing the next config-less
|
|
924
|
+
// call to restore the internal default policy. See GHSA-vxr8-fq34-vvx9.
|
|
925
|
+
trustedTypesPolicy = undefined;
|
|
926
|
+
emptyHTML = '';
|
|
813
927
|
} else {
|
|
814
|
-
//
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
);
|
|
928
|
+
// No policy supplied: keep the currently active policy if one is set — a
|
|
929
|
+
// previously supplied policy is intentionally sticky across config-less
|
|
930
|
+
// calls — otherwise fall back to the instance's own internal policy,
|
|
931
|
+
// created at most once. (A policy supplied for a *single* call still
|
|
932
|
+
// lingers by design; what must not linger is a policy whose configuration
|
|
933
|
+
// has been torn down via `clearConfig()`, which restores the default.)
|
|
934
|
+
if (trustedTypesPolicy === undefined) {
|
|
935
|
+
trustedTypesPolicy = _getDefaultTrustedTypesPolicy();
|
|
823
936
|
}
|
|
824
937
|
|
|
825
|
-
//
|
|
826
|
-
//
|
|
827
|
-
//
|
|
828
|
-
// policy
|
|
829
|
-
// would call `.createHTML` on a non-policy and throw. See #1422.
|
|
938
|
+
// Sign internal variables only when a policy is active. A falsy policy
|
|
939
|
+
// (Trusted Types unsupported, creation failed, or an explicit opt-out)
|
|
940
|
+
// leaves `emptyHTML` as a plain string, so we never call `.createHTML` on
|
|
941
|
+
// a non-policy and throw. See #1422.
|
|
830
942
|
if (trustedTypesPolicy && typeof emptyHTML === 'string') {
|
|
831
943
|
emptyHTML = _createTrustedHTML('');
|
|
832
944
|
}
|
|
@@ -878,6 +990,111 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
878
990
|
...TAGS.mathMlDisallowed,
|
|
879
991
|
]);
|
|
880
992
|
|
|
993
|
+
/**
|
|
994
|
+
* Namespace rules for an element in the SVG 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 _checkSvgNamespace = function (
|
|
1002
|
+
tagName: string,
|
|
1003
|
+
parent: { namespaceURI?: string },
|
|
1004
|
+
parentTagName: string
|
|
1005
|
+
): boolean {
|
|
1006
|
+
// The only way to switch from HTML namespace to SVG
|
|
1007
|
+
// is via <svg>. If it happens via any other tag, then
|
|
1008
|
+
// it should be killed.
|
|
1009
|
+
if (parent.namespaceURI === HTML_NAMESPACE) {
|
|
1010
|
+
return tagName === 'svg';
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// The only way to switch from MathML to SVG is via <svg>
|
|
1014
|
+
// if the parent is either <annotation-xml> or a MathML
|
|
1015
|
+
// text integration point.
|
|
1016
|
+
if (parent.namespaceURI === MATHML_NAMESPACE) {
|
|
1017
|
+
return (
|
|
1018
|
+
tagName === 'svg' &&
|
|
1019
|
+
(parentTagName === 'annotation-xml' ||
|
|
1020
|
+
MATHML_TEXT_INTEGRATION_POINTS[parentTagName])
|
|
1021
|
+
);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// We only allow elements that are defined in SVG
|
|
1025
|
+
// spec. All others are disallowed in SVG namespace.
|
|
1026
|
+
return Boolean(ALL_SVG_TAGS[tagName]);
|
|
1027
|
+
};
|
|
1028
|
+
|
|
1029
|
+
/**
|
|
1030
|
+
* Namespace rules for an element in the MathML namespace.
|
|
1031
|
+
*
|
|
1032
|
+
* @param tagName the element's lowercase tag name
|
|
1033
|
+
* @param parent the (possibly simulated) parent node
|
|
1034
|
+
* @param parentTagName the parent's lowercase tag name
|
|
1035
|
+
* @returns true if a spec-compliant parser could produce this element
|
|
1036
|
+
*/
|
|
1037
|
+
const _checkMathMlNamespace = function (
|
|
1038
|
+
tagName: string,
|
|
1039
|
+
parent: { namespaceURI?: string },
|
|
1040
|
+
parentTagName: string
|
|
1041
|
+
): boolean {
|
|
1042
|
+
// The only way to switch from HTML namespace to MathML
|
|
1043
|
+
// is via <math>. If it happens via any other tag, then
|
|
1044
|
+
// it should be killed.
|
|
1045
|
+
if (parent.namespaceURI === HTML_NAMESPACE) {
|
|
1046
|
+
return tagName === 'math';
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// The only way to switch from SVG to MathML is via
|
|
1050
|
+
// <math> and HTML integration points
|
|
1051
|
+
if (parent.namespaceURI === SVG_NAMESPACE) {
|
|
1052
|
+
return tagName === 'math' && HTML_INTEGRATION_POINTS[parentTagName];
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// We only allow elements that are defined in MathML
|
|
1056
|
+
// spec. All others are disallowed in MathML namespace.
|
|
1057
|
+
return Boolean(ALL_MATHML_TAGS[tagName]);
|
|
1058
|
+
};
|
|
1059
|
+
|
|
1060
|
+
/**
|
|
1061
|
+
* Namespace rules for an element in the HTML namespace.
|
|
1062
|
+
*
|
|
1063
|
+
* @param tagName the element's lowercase tag name
|
|
1064
|
+
* @param parent the (possibly simulated) parent node
|
|
1065
|
+
* @param parentTagName the parent's lowercase tag name
|
|
1066
|
+
* @returns true if a spec-compliant parser could produce this element
|
|
1067
|
+
*/
|
|
1068
|
+
const _checkHtmlNamespace = function (
|
|
1069
|
+
tagName: string,
|
|
1070
|
+
parent: { namespaceURI?: string },
|
|
1071
|
+
parentTagName: string
|
|
1072
|
+
): boolean {
|
|
1073
|
+
// The only way to switch from SVG to HTML is via
|
|
1074
|
+
// HTML integration points, and from MathML to HTML
|
|
1075
|
+
// is via MathML text integration points
|
|
1076
|
+
if (
|
|
1077
|
+
parent.namespaceURI === SVG_NAMESPACE &&
|
|
1078
|
+
!HTML_INTEGRATION_POINTS[parentTagName]
|
|
1079
|
+
) {
|
|
1080
|
+
return false;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
if (
|
|
1084
|
+
parent.namespaceURI === MATHML_NAMESPACE &&
|
|
1085
|
+
!MATHML_TEXT_INTEGRATION_POINTS[parentTagName]
|
|
1086
|
+
) {
|
|
1087
|
+
return false;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// We disallow tags that are specific for MathML
|
|
1091
|
+
// or SVG and should never appear in HTML namespace
|
|
1092
|
+
return (
|
|
1093
|
+
!ALL_MATHML_TAGS[tagName] &&
|
|
1094
|
+
(COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName])
|
|
1095
|
+
);
|
|
1096
|
+
};
|
|
1097
|
+
|
|
881
1098
|
/**
|
|
882
1099
|
* @param element a DOM element whose namespace is being checked
|
|
883
1100
|
* @returns Return false if the element has a
|
|
@@ -904,72 +1121,15 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
904
1121
|
}
|
|
905
1122
|
|
|
906
1123
|
if (element.namespaceURI === SVG_NAMESPACE) {
|
|
907
|
-
|
|
908
|
-
// is via <svg>. If it happens via any other tag, then
|
|
909
|
-
// it should be killed.
|
|
910
|
-
if (parent.namespaceURI === HTML_NAMESPACE) {
|
|
911
|
-
return tagName === 'svg';
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
// The only way to switch from MathML to SVG is via`
|
|
915
|
-
// svg if parent is either <annotation-xml> or MathML
|
|
916
|
-
// text integration points.
|
|
917
|
-
if (parent.namespaceURI === MATHML_NAMESPACE) {
|
|
918
|
-
return (
|
|
919
|
-
tagName === 'svg' &&
|
|
920
|
-
(parentTagName === 'annotation-xml' ||
|
|
921
|
-
MATHML_TEXT_INTEGRATION_POINTS[parentTagName])
|
|
922
|
-
);
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
// We only allow elements that are defined in SVG
|
|
926
|
-
// spec. All others are disallowed in SVG namespace.
|
|
927
|
-
return Boolean(ALL_SVG_TAGS[tagName]);
|
|
1124
|
+
return _checkSvgNamespace(tagName, parent, parentTagName);
|
|
928
1125
|
}
|
|
929
1126
|
|
|
930
1127
|
if (element.namespaceURI === MATHML_NAMESPACE) {
|
|
931
|
-
|
|
932
|
-
// is via <math>. If it happens via any other tag, then
|
|
933
|
-
// it should be killed.
|
|
934
|
-
if (parent.namespaceURI === HTML_NAMESPACE) {
|
|
935
|
-
return tagName === 'math';
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
// The only way to switch from SVG to MathML is via
|
|
939
|
-
// <math> and HTML integration points
|
|
940
|
-
if (parent.namespaceURI === SVG_NAMESPACE) {
|
|
941
|
-
return tagName === 'math' && HTML_INTEGRATION_POINTS[parentTagName];
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
// We only allow elements that are defined in MathML
|
|
945
|
-
// spec. All others are disallowed in MathML namespace.
|
|
946
|
-
return Boolean(ALL_MATHML_TAGS[tagName]);
|
|
1128
|
+
return _checkMathMlNamespace(tagName, parent, parentTagName);
|
|
947
1129
|
}
|
|
948
1130
|
|
|
949
1131
|
if (element.namespaceURI === HTML_NAMESPACE) {
|
|
950
|
-
|
|
951
|
-
// HTML integration points, and from MathML to HTML
|
|
952
|
-
// is via MathML text integration points
|
|
953
|
-
if (
|
|
954
|
-
parent.namespaceURI === SVG_NAMESPACE &&
|
|
955
|
-
!HTML_INTEGRATION_POINTS[parentTagName]
|
|
956
|
-
) {
|
|
957
|
-
return false;
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
if (
|
|
961
|
-
parent.namespaceURI === MATHML_NAMESPACE &&
|
|
962
|
-
!MATHML_TEXT_INTEGRATION_POINTS[parentTagName]
|
|
963
|
-
) {
|
|
964
|
-
return false;
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
// We disallow tags that are specific for MathML
|
|
968
|
-
// or SVG and should never appear in HTML namespace
|
|
969
|
-
return (
|
|
970
|
-
!ALL_MATHML_TAGS[tagName] &&
|
|
971
|
-
(COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName])
|
|
972
|
-
);
|
|
1132
|
+
return _checkHtmlNamespace(tagName, parent, parentTagName);
|
|
973
1133
|
}
|
|
974
1134
|
|
|
975
1135
|
// For XHTML and XML documents that support custom namespaces
|
|
@@ -999,7 +1159,81 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
999
1159
|
// eslint-disable-next-line unicorn/prefer-dom-node-remove
|
|
1000
1160
|
getParentNode(node).removeChild(node);
|
|
1001
1161
|
} catch (_) {
|
|
1162
|
+
/* The normal detach failed — this is reached for a parentless node
|
|
1163
|
+
(getParentNode() is null, so .removeChild throws). Element.prototype
|
|
1164
|
+
.remove() is itself a spec no-op on a parentless node, so a recorded
|
|
1165
|
+
"removal" would otherwise hand the caller back an intact,
|
|
1166
|
+
payload-bearing node (e.g. a detached IN_PLACE root the mXSS canary or
|
|
1167
|
+
the style-with-element-child rule decided to kill). Fail closed by
|
|
1168
|
+
throwing — exactly as a clobbered root does at the IN_PLACE entry —
|
|
1169
|
+
rather than trying to "neutralize" the node via its own methods.
|
|
1170
|
+
Neutralizing would mean calling getAttributeNames()/removeAttribute()
|
|
1171
|
+
on the node, both of which a <form> root can clobber via a named child
|
|
1172
|
+
(and _isClobbered does not even probe getAttributeNames), so the
|
|
1173
|
+
neutralize step could itself be silently defeated, leaving the payload
|
|
1174
|
+
intact. A throw touches only the cached, clobber-safe remove() and
|
|
1175
|
+
getParentNode(). Generalizes GHSA-r47g-fvhr-h676 (clobbered-form root)
|
|
1176
|
+
to every root-kill reason. REPORT-3.
|
|
1177
|
+
|
|
1178
|
+
This lives inside the catch, so it never fires for a normally-removed
|
|
1179
|
+
in-tree node: those have a parent, removeChild() succeeds, and the
|
|
1180
|
+
catch is not entered. Only a kept (parentless) root reaches here. */
|
|
1002
1181
|
remove(node);
|
|
1182
|
+
|
|
1183
|
+
if (!getParentNode(node)) {
|
|
1184
|
+
throw typeErrorCreate(
|
|
1185
|
+
'a node selected for removal could not be detached from its tree ' +
|
|
1186
|
+
'and cannot be safely returned; refusing to sanitize in place'
|
|
1187
|
+
);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
};
|
|
1191
|
+
|
|
1192
|
+
/**
|
|
1193
|
+
* _neutralizeRoot
|
|
1194
|
+
*
|
|
1195
|
+
* Fail-closed teardown of an in-place root after the sanitize walk aborts
|
|
1196
|
+
* (campaign-3 F2). An internal throw mid-walk — e.g. a page-registered
|
|
1197
|
+
* custom element's reaction detaches a node so `_forceRemove`'s deliberate
|
|
1198
|
+
* parentless guard throws, or any other re-entrant engine mutation — would
|
|
1199
|
+
* otherwise leave the caller's *live* tree half-sanitized, with everything
|
|
1200
|
+
* after the abort point still carrying its handlers. There is no safe way
|
|
1201
|
+
* to resume the walk (the tree mutated under us), so we strip the root bare:
|
|
1202
|
+
* remove every child and every attribute, then let the caller's catch see
|
|
1203
|
+
* the original error. Clobber-safe (cached `remove`/`childNodes`/`attributes`
|
|
1204
|
+
* getters; the root was already clobber-pre-flighted at the IN_PLACE entry).
|
|
1205
|
+
*
|
|
1206
|
+
* @param root the in-place root to empty
|
|
1207
|
+
*/
|
|
1208
|
+
const _neutralizeRoot = function (root: Node): void {
|
|
1209
|
+
const childNodes = getChildNodes(root);
|
|
1210
|
+
if (childNodes) {
|
|
1211
|
+
const snapshot: Node[] = [];
|
|
1212
|
+
arrayForEach(childNodes, (child) => {
|
|
1213
|
+
arrayPush(snapshot, child);
|
|
1214
|
+
});
|
|
1215
|
+
arrayForEach(snapshot, (child) => {
|
|
1216
|
+
try {
|
|
1217
|
+
remove(child);
|
|
1218
|
+
} catch (_) {
|
|
1219
|
+
/* Best-effort teardown; a still-attached child is handled below */
|
|
1220
|
+
}
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
const attributes = getAttributes(root);
|
|
1225
|
+
if (attributes) {
|
|
1226
|
+
for (let i = attributes.length - 1; i >= 0; --i) {
|
|
1227
|
+
const attribute = attributes[i];
|
|
1228
|
+
const name = attribute && attribute.name;
|
|
1229
|
+
if (typeof name === 'string') {
|
|
1230
|
+
try {
|
|
1231
|
+
(root as Element).removeAttribute(name);
|
|
1232
|
+
} catch (_) {
|
|
1233
|
+
/* Clobbered removeAttribute — ignore (fail-closed best effort) */
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1003
1237
|
}
|
|
1004
1238
|
};
|
|
1005
1239
|
|
|
@@ -1038,6 +1272,79 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
1038
1272
|
}
|
|
1039
1273
|
};
|
|
1040
1274
|
|
|
1275
|
+
/**
|
|
1276
|
+
* _stripDisallowedAttributes
|
|
1277
|
+
*
|
|
1278
|
+
* Removes every attribute the active configuration does not allow from a
|
|
1279
|
+
* single element, using the same allowlist as the main attribute pass (so
|
|
1280
|
+
* `on*` handlers go, but no `/^on/` blocklist is introduced). Used only to
|
|
1281
|
+
* neutralise nodes that are being discarded from an in-place tree.
|
|
1282
|
+
*
|
|
1283
|
+
* @param element the element to strip
|
|
1284
|
+
*/
|
|
1285
|
+
const _stripDisallowedAttributes = function (element: Element): void {
|
|
1286
|
+
const attributes = getAttributes(element);
|
|
1287
|
+
if (!attributes) {
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
for (let i = attributes.length - 1; i >= 0; --i) {
|
|
1292
|
+
const attribute = attributes[i];
|
|
1293
|
+
const name = attribute && attribute.name;
|
|
1294
|
+
if (typeof name !== 'string' || ALLOWED_ATTR[transformCaseFunc(name)]) {
|
|
1295
|
+
continue;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
try {
|
|
1299
|
+
element.removeAttribute(name);
|
|
1300
|
+
} catch (_) {
|
|
1301
|
+
/* Clobbered removeAttribute on a doomed node — ignore */
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
};
|
|
1305
|
+
|
|
1306
|
+
/**
|
|
1307
|
+
* _neutralizeSubtree
|
|
1308
|
+
*
|
|
1309
|
+
* Completes the audit-5 F1 fix across every removal path. The KEEP_CONTENT
|
|
1310
|
+
* move-hoist neutralises only disallowed-tag removals; clobber, mXSS-canary,
|
|
1311
|
+
* namespace, comment, processing-instruction and KEEP_CONTENT:false removals
|
|
1312
|
+
* all drop their subtree wholesale via `_forceRemove`. On the IN_PLACE path
|
|
1313
|
+
* those dropped nodes are detached from the caller's LIVE tree but a
|
|
1314
|
+
* handler-bearing original among them (an `<img onerror>`/`<video>` that was
|
|
1315
|
+
* loading) keeps its queued resource event, which fires in page scope after
|
|
1316
|
+
* sanitize returns. This walks a removed subtree and strips every attribute
|
|
1317
|
+
* the active configuration does not allow — so `on*` handlers are cancelled
|
|
1318
|
+
* through the SAME allowlist that governs kept nodes, not a separate `/^on/`
|
|
1319
|
+
* blocklist. Run synchronously before sanitize returns, i.e. before any
|
|
1320
|
+
* queued event can fire. Hook-free by design: these nodes leave the output,
|
|
1321
|
+
* so firing attribute hooks for them would be surprising. Clobber-safe reads;
|
|
1322
|
+
* a doomed clobbered node may shadow `removeAttribute` (its own attributes are
|
|
1323
|
+
* irrelevant — it is discarded — while its non-clobbered descendants, e.g.
|
|
1324
|
+
* the `<img>`, are reached and scrubbed).
|
|
1325
|
+
*
|
|
1326
|
+
* @param root the root of a removed subtree to neutralise
|
|
1327
|
+
*/
|
|
1328
|
+
const _neutralizeSubtree = function (root: Node): void {
|
|
1329
|
+
const stack: Node[] = [root];
|
|
1330
|
+
|
|
1331
|
+
while (stack.length > 0) {
|
|
1332
|
+
const node = stack.pop();
|
|
1333
|
+
const nodeType = getNodeType ? getNodeType(node) : (node as any).nodeType;
|
|
1334
|
+
|
|
1335
|
+
if (nodeType === NODE_TYPE.element) {
|
|
1336
|
+
_stripDisallowedAttributes(node as Element);
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
const childNodes = getChildNodes(node);
|
|
1340
|
+
if (childNodes) {
|
|
1341
|
+
for (let i = childNodes.length - 1; i >= 0; --i) {
|
|
1342
|
+
stack.push(childNodes[i]);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
};
|
|
1347
|
+
|
|
1041
1348
|
/**
|
|
1042
1349
|
* _initDocument
|
|
1043
1350
|
*
|
|
@@ -1131,6 +1438,21 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
1131
1438
|
);
|
|
1132
1439
|
};
|
|
1133
1440
|
|
|
1441
|
+
/**
|
|
1442
|
+
* Replace template expression syntax (mustache, ERB, template
|
|
1443
|
+
* literal) with a space; shared by all SAFE_FOR_TEMPLATES scrub
|
|
1444
|
+
* sites. Order matters: mustache, then ERB, then template literal.
|
|
1445
|
+
*
|
|
1446
|
+
* @param value the string to scrub
|
|
1447
|
+
* @returns the scrubbed string
|
|
1448
|
+
*/
|
|
1449
|
+
const _stripTemplateExpressions = function (value: string): string {
|
|
1450
|
+
value = stringReplace(value, MUSTACHE_EXPR, ' ');
|
|
1451
|
+
value = stringReplace(value, ERB_EXPR, ' ');
|
|
1452
|
+
value = stringReplace(value, TMPLIT_EXPR, ' ');
|
|
1453
|
+
return value;
|
|
1454
|
+
};
|
|
1455
|
+
|
|
1134
1456
|
/**
|
|
1135
1457
|
* Strip template-engine expressions ({{...}}, ${...}, <%...%>) from the
|
|
1136
1458
|
* character data of an element subtree. Used as the final safety net for
|
|
@@ -1165,23 +1487,21 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
1165
1487
|
|
|
1166
1488
|
let currentNode = walker.nextNode() as CharacterData | null;
|
|
1167
1489
|
while (currentNode) {
|
|
1168
|
-
|
|
1169
|
-
arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], (expr: RegExp) => {
|
|
1170
|
-
data = stringReplace(data, expr, ' ');
|
|
1171
|
-
});
|
|
1172
|
-
currentNode.data = data;
|
|
1490
|
+
currentNode.data = _stripTemplateExpressions(currentNode.data);
|
|
1173
1491
|
currentNode = walker.nextNode() as CharacterData | null;
|
|
1174
1492
|
}
|
|
1175
1493
|
|
|
1176
1494
|
// NodeIterator does not descend into <template>.content per the DOM spec,
|
|
1177
1495
|
// so we must explicitly recurse into each template's content fragment,
|
|
1178
1496
|
// mirroring the approach used by _sanitizeShadowDOM.
|
|
1179
|
-
const templates = node.querySelectorAll?.('template')
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1497
|
+
const templates = node.querySelectorAll?.('template');
|
|
1498
|
+
if (templates) {
|
|
1499
|
+
arrayForEach(templates, (tmpl: HTMLTemplateElement) => {
|
|
1500
|
+
if (_isDocumentFragment(tmpl.content)) {
|
|
1501
|
+
_scrubTemplateExpressions(tmpl.content as unknown as Element);
|
|
1502
|
+
}
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1185
1505
|
};
|
|
1186
1506
|
|
|
1187
1507
|
/**
|
|
@@ -1298,52 +1618,34 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
1298
1618
|
currentNode: Parameters<T>[0],
|
|
1299
1619
|
data: Parameters<T>[1]
|
|
1300
1620
|
): void {
|
|
1621
|
+
if (hooks.length === 0) {
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1301
1625
|
arrayForEach(hooks, (hook: T) => {
|
|
1302
1626
|
hook.call(DOMPurify, currentNode, data, CONFIG);
|
|
1303
1627
|
});
|
|
1304
1628
|
}
|
|
1305
1629
|
|
|
1306
1630
|
/**
|
|
1307
|
-
*
|
|
1631
|
+
* Structural-threat checks that condemn a node regardless of the
|
|
1632
|
+
* allowlists: mXSS via namespace confusion, risky CSS construction,
|
|
1633
|
+
* processing instructions, markup-bearing comments. Pure predicate;
|
|
1634
|
+
* the caller removes. Check order is load-bearing.
|
|
1308
1635
|
*
|
|
1309
|
-
* @
|
|
1310
|
-
* @
|
|
1311
|
-
* @
|
|
1312
|
-
* @param currentNode to check for permission to exist
|
|
1313
|
-
* @return true if node was killed, false if left alive
|
|
1636
|
+
* @param currentNode the node to inspect
|
|
1637
|
+
* @param tagName the node's transformCaseFunc'd tag name
|
|
1638
|
+
* @return true if the node must be removed
|
|
1314
1639
|
*/
|
|
1315
|
-
const
|
|
1316
|
-
let content = null;
|
|
1317
|
-
|
|
1318
|
-
/* Execute a hook if present */
|
|
1319
|
-
_executeHooks(hooks.beforeSanitizeElements, currentNode, null);
|
|
1320
|
-
|
|
1321
|
-
/* Check if element is clobbered or can clobber */
|
|
1322
|
-
if (_isClobbered(currentNode)) {
|
|
1323
|
-
_forceRemove(currentNode);
|
|
1324
|
-
return true;
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
/* Now let's check the element's type and name */
|
|
1328
|
-
const tagName = transformCaseFunc(
|
|
1329
|
-
getNodeName ? getNodeName(currentNode) : currentNode.nodeName
|
|
1330
|
-
);
|
|
1331
|
-
|
|
1332
|
-
/* Execute a hook if present */
|
|
1333
|
-
_executeHooks(hooks.uponSanitizeElement, currentNode, {
|
|
1334
|
-
tagName,
|
|
1335
|
-
allowedTags: ALLOWED_TAGS,
|
|
1336
|
-
});
|
|
1337
|
-
|
|
1640
|
+
const _isUnsafeNode = function (currentNode: any, tagName: string): boolean {
|
|
1338
1641
|
/* Detect mXSS attempts abusing namespace confusion */
|
|
1339
1642
|
if (
|
|
1340
1643
|
SAFE_FOR_XML &&
|
|
1341
1644
|
currentNode.hasChildNodes() &&
|
|
1342
1645
|
!_isNode(currentNode.firstElementChild) &&
|
|
1343
|
-
regExpTest(
|
|
1344
|
-
regExpTest(
|
|
1646
|
+
regExpTest(EXPRESSIONS.ELEMENT_MARKUP_PROBE, currentNode.textContent) &&
|
|
1647
|
+
regExpTest(EXPRESSIONS.ELEMENT_MARKUP_PROBE, currentNode.innerHTML)
|
|
1345
1648
|
) {
|
|
1346
|
-
_forceRemove(currentNode);
|
|
1347
1649
|
return true;
|
|
1348
1650
|
}
|
|
1349
1651
|
|
|
@@ -1354,13 +1656,11 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
1354
1656
|
tagName === 'style' &&
|
|
1355
1657
|
_isNode(currentNode.firstElementChild)
|
|
1356
1658
|
) {
|
|
1357
|
-
_forceRemove(currentNode);
|
|
1358
1659
|
return true;
|
|
1359
1660
|
}
|
|
1360
1661
|
|
|
1361
1662
|
/* Remove any occurrence of processing instructions */
|
|
1362
|
-
if (currentNode.nodeType === NODE_TYPE.
|
|
1363
|
-
_forceRemove(currentNode);
|
|
1663
|
+
if (currentNode.nodeType === NODE_TYPE.processingInstruction) {
|
|
1364
1664
|
return true;
|
|
1365
1665
|
}
|
|
1366
1666
|
|
|
@@ -1368,39 +1668,47 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
1368
1668
|
if (
|
|
1369
1669
|
SAFE_FOR_XML &&
|
|
1370
1670
|
currentNode.nodeType === NODE_TYPE.comment &&
|
|
1371
|
-
regExpTest(
|
|
1671
|
+
regExpTest(EXPRESSIONS.COMMENT_MARKUP_PROBE, currentNode.data)
|
|
1372
1672
|
) {
|
|
1373
|
-
_forceRemove(currentNode);
|
|
1374
1673
|
return true;
|
|
1375
1674
|
}
|
|
1376
1675
|
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
FORBID_TAGS[tagName] ||
|
|
1380
|
-
(!(
|
|
1381
|
-
EXTRA_ELEMENT_HANDLING.tagCheck instanceof Function &&
|
|
1382
|
-
EXTRA_ELEMENT_HANDLING.tagCheck(tagName)
|
|
1383
|
-
) &&
|
|
1384
|
-
!ALLOWED_TAGS[tagName])
|
|
1385
|
-
) {
|
|
1386
|
-
/* Check if we have a custom element to handle */
|
|
1387
|
-
if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {
|
|
1388
|
-
if (
|
|
1389
|
-
CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp &&
|
|
1390
|
-
regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)
|
|
1391
|
-
) {
|
|
1392
|
-
return false;
|
|
1393
|
-
}
|
|
1676
|
+
return false;
|
|
1677
|
+
};
|
|
1394
1678
|
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1679
|
+
/**
|
|
1680
|
+
* Handle a node whose tag is forbidden or not allowlisted: keep
|
|
1681
|
+
* allowed custom elements (false return exits _sanitizeElements
|
|
1682
|
+
* early - namespace/fallback checks and the afterSanitizeElements
|
|
1683
|
+
* hook are intentionally skipped for kept custom elements), else
|
|
1684
|
+
* hoist content per KEEP_CONTENT and remove.
|
|
1685
|
+
*
|
|
1686
|
+
* @param currentNode the disallowed node
|
|
1687
|
+
* @param tagName the node's transformCaseFunc'd tag name
|
|
1688
|
+
* @return true if the node was removed, false if kept
|
|
1689
|
+
*/
|
|
1690
|
+
const _sanitizeDisallowedNode = function (
|
|
1691
|
+
currentNode: any,
|
|
1692
|
+
tagName: string
|
|
1693
|
+
): boolean {
|
|
1694
|
+
/* Check if we have a custom element to handle */
|
|
1695
|
+
if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {
|
|
1696
|
+
if (
|
|
1697
|
+
CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp &&
|
|
1698
|
+
regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)
|
|
1699
|
+
) {
|
|
1700
|
+
return false;
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
if (
|
|
1704
|
+
CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function &&
|
|
1705
|
+
CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)
|
|
1706
|
+
) {
|
|
1707
|
+
return false;
|
|
1401
1708
|
}
|
|
1709
|
+
}
|
|
1402
1710
|
|
|
1403
|
-
|
|
1711
|
+
/* Keep content except for bad-listed elements.
|
|
1404
1712
|
Use the cached prototype getters exclusively — the previous code
|
|
1405
1713
|
had `|| currentNode.parentNode` / `|| currentNode.childNodes`
|
|
1406
1714
|
fallbacks, but the cached getters always return the canonical
|
|
@@ -1408,24 +1716,95 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
1408
1716
|
path was dead in safe cases and a clobbering surface in unsafe
|
|
1409
1717
|
ones. Falsy cached results stay falsy; the `if (childNodes &&
|
|
1410
1718
|
parentNode)` check already gates correctly. */
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1719
|
+
if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {
|
|
1720
|
+
const parentNode = getParentNode(currentNode);
|
|
1721
|
+
const childNodes = getChildNodes(currentNode);
|
|
1722
|
+
|
|
1723
|
+
if (childNodes && parentNode) {
|
|
1724
|
+
const childCount = childNodes.length;
|
|
1725
|
+
|
|
1726
|
+
/* In-place: hoist the *original* children so the iterator visits
|
|
1727
|
+
and sanitises them through the same allowlist pass as every other
|
|
1728
|
+
node. The caller built the tree in the live document, so the
|
|
1729
|
+
originals carry already-queued resource events (`<img onerror>`,
|
|
1730
|
+
`<video>`/`<audio>` error, lazy/`onload`, …); cloning would leave
|
|
1731
|
+
those originals detached but still armed, firing in page scope
|
|
1732
|
+
while the returned tree looked clean. Moving is safe in-place: the
|
|
1733
|
+
root is pre-validated as an allowed tag and so is never the node
|
|
1734
|
+
being removed, which keeps `parentNode` inside the iterator root
|
|
1735
|
+
and the relocated child inside the serialised tree.
|
|
1736
|
+
|
|
1737
|
+
Otherwise (string / DOM-copy paths): clone. The iterator is rooted
|
|
1738
|
+
at — and the result serialised from — `body`, so a restrictive
|
|
1739
|
+
ALLOWED_TAGS that removes `body` itself must leave its content in
|
|
1740
|
+
place, which only cloning does; and those paths parse into an
|
|
1741
|
+
inert document, so their discarded originals never had a queued
|
|
1742
|
+
event to neutralise.
|
|
1743
|
+
|
|
1744
|
+
`childNodes` is live; a tail-to-head walk keeps `childNodes[i]`
|
|
1745
|
+
valid whether we move (drops the trailing entry) or clone (leaves
|
|
1746
|
+
the list intact). */
|
|
1747
|
+
for (let i = childCount - 1; i >= 0; --i) {
|
|
1748
|
+
const hoisted = IN_PLACE
|
|
1749
|
+
? childNodes[i]
|
|
1750
|
+
: cloneNode(childNodes[i], true);
|
|
1751
|
+
parentNode.insertBefore(hoisted, getNextSibling(currentNode));
|
|
1422
1752
|
}
|
|
1423
1753
|
}
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
_forceRemove(currentNode);
|
|
1757
|
+
return true;
|
|
1758
|
+
};
|
|
1424
1759
|
|
|
1760
|
+
/**
|
|
1761
|
+
* _sanitizeElements
|
|
1762
|
+
*
|
|
1763
|
+
* @protect nodeName
|
|
1764
|
+
* @protect textContent
|
|
1765
|
+
* @protect removeChild
|
|
1766
|
+
* @param currentNode to check for permission to exist
|
|
1767
|
+
* @return true if node was killed, false if left alive
|
|
1768
|
+
*/
|
|
1769
|
+
const _sanitizeElements = function (currentNode: any): boolean {
|
|
1770
|
+
/* Execute a hook if present */
|
|
1771
|
+
_executeHooks(hooks.beforeSanitizeElements, currentNode, null);
|
|
1772
|
+
|
|
1773
|
+
/* Check if element is clobbered or can clobber */
|
|
1774
|
+
if (_isClobbered(currentNode)) {
|
|
1775
|
+
_forceRemove(currentNode);
|
|
1776
|
+
return true;
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
/* Now let's check the element's type and name */
|
|
1780
|
+
const tagName = transformCaseFunc(
|
|
1781
|
+
getNodeName ? getNodeName(currentNode) : currentNode.nodeName
|
|
1782
|
+
);
|
|
1783
|
+
|
|
1784
|
+
/* Execute a hook if present */
|
|
1785
|
+
_executeHooks(hooks.uponSanitizeElement, currentNode, {
|
|
1786
|
+
tagName,
|
|
1787
|
+
allowedTags: ALLOWED_TAGS,
|
|
1788
|
+
});
|
|
1789
|
+
|
|
1790
|
+
/* Remove mXSS vectors, processing instructions and risky comments */
|
|
1791
|
+
if (_isUnsafeNode(currentNode, tagName)) {
|
|
1425
1792
|
_forceRemove(currentNode);
|
|
1426
1793
|
return true;
|
|
1427
1794
|
}
|
|
1428
1795
|
|
|
1796
|
+
/* Remove element if anything forbids its presence */
|
|
1797
|
+
if (
|
|
1798
|
+
FORBID_TAGS[tagName] ||
|
|
1799
|
+
(!(
|
|
1800
|
+
EXTRA_ELEMENT_HANDLING.tagCheck instanceof Function &&
|
|
1801
|
+
EXTRA_ELEMENT_HANDLING.tagCheck(tagName)
|
|
1802
|
+
) &&
|
|
1803
|
+
!ALLOWED_TAGS[tagName])
|
|
1804
|
+
) {
|
|
1805
|
+
return _sanitizeDisallowedNode(currentNode, tagName);
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1429
1808
|
/* Check whether element has a valid namespace.
|
|
1430
1809
|
Realm-safe check (GHSA-hpcv-96wg-7vj8): use the cached Node.prototype
|
|
1431
1810
|
nodeType getter rather than `instanceof Element`, which is realm-
|
|
@@ -1443,7 +1822,7 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
1443
1822
|
(tagName === 'noscript' ||
|
|
1444
1823
|
tagName === 'noembed' ||
|
|
1445
1824
|
tagName === 'noframes') &&
|
|
1446
|
-
regExpTest(
|
|
1825
|
+
regExpTest(EXPRESSIONS.FALLBACK_TAG_CLOSE, currentNode.innerHTML)
|
|
1447
1826
|
) {
|
|
1448
1827
|
_forceRemove(currentNode);
|
|
1449
1828
|
return true;
|
|
@@ -1452,11 +1831,7 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
1452
1831
|
/* Sanitize element content to be template-safe */
|
|
1453
1832
|
if (SAFE_FOR_TEMPLATES && currentNode.nodeType === NODE_TYPE.text) {
|
|
1454
1833
|
/* Get the element's text content */
|
|
1455
|
-
content = currentNode.textContent;
|
|
1456
|
-
|
|
1457
|
-
arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], (expr: RegExp) => {
|
|
1458
|
-
content = stringReplace(content, expr, ' ');
|
|
1459
|
-
});
|
|
1834
|
+
const content = _stripTemplateExpressions(currentNode.textContent);
|
|
1460
1835
|
|
|
1461
1836
|
if (currentNode.textContent !== content) {
|
|
1462
1837
|
arrayPush(DOMPurify.removed, { element: currentNode.cloneNode() });
|
|
@@ -1507,16 +1882,12 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
1507
1882
|
(https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes)
|
|
1508
1883
|
XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804)
|
|
1509
1884
|
We don't need to check the value; it's always URI safe. */
|
|
1510
|
-
if (
|
|
1511
|
-
ALLOW_DATA_ATTR &&
|
|
1512
|
-
!FORBID_ATTR[lcName] &&
|
|
1513
|
-
regExpTest(DATA_ATTR, lcName)
|
|
1514
|
-
) {
|
|
1885
|
+
if (ALLOW_DATA_ATTR && regExpTest(DATA_ATTR, lcName)) {
|
|
1515
1886
|
// This attribute is safe
|
|
1516
1887
|
} else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR, lcName)) {
|
|
1517
1888
|
// This attribute is safe
|
|
1518
1889
|
/* Otherwise, check the name is permitted */
|
|
1519
|
-
} else if (!nameIsPermitted
|
|
1890
|
+
} else if (!nameIsPermitted) {
|
|
1520
1891
|
if (
|
|
1521
1892
|
// First condition does a very basic check if a) it's basically a valid custom element tagname AND
|
|
1522
1893
|
// b) if the tagName passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck
|
|
@@ -1610,6 +1981,85 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
1610
1981
|
);
|
|
1611
1982
|
};
|
|
1612
1983
|
|
|
1984
|
+
/**
|
|
1985
|
+
* Wrap an attribute value in the matching Trusted Types object when
|
|
1986
|
+
* the active policy requires it. Namespaced attributes pass through
|
|
1987
|
+
* unchanged (no TT support yet, see
|
|
1988
|
+
* https://bugs.chromium.org/p/chromium/issues/detail?id=1305293).
|
|
1989
|
+
*
|
|
1990
|
+
* @param lcTag lowercase tag name of the containing element
|
|
1991
|
+
* @param lcName lowercase attribute name
|
|
1992
|
+
* @param namespaceURI the attribute's namespace, if any
|
|
1993
|
+
* @param value the attribute value to wrap
|
|
1994
|
+
* @return the value, wrapped when Trusted Types demand it
|
|
1995
|
+
*/
|
|
1996
|
+
const _applyTrustedTypesToAttribute = function (
|
|
1997
|
+
lcTag: string,
|
|
1998
|
+
lcName: string,
|
|
1999
|
+
namespaceURI: string | null,
|
|
2000
|
+
value: string
|
|
2001
|
+
): string {
|
|
2002
|
+
if (
|
|
2003
|
+
trustedTypesPolicy &&
|
|
2004
|
+
typeof trustedTypes === 'object' &&
|
|
2005
|
+
typeof trustedTypes.getAttributeType === 'function' &&
|
|
2006
|
+
!namespaceURI
|
|
2007
|
+
) {
|
|
2008
|
+
switch (trustedTypes.getAttributeType(lcTag, lcName)) {
|
|
2009
|
+
case 'TrustedHTML': {
|
|
2010
|
+
return _createTrustedHTML(value);
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
case 'TrustedScriptURL': {
|
|
2014
|
+
return _createTrustedScriptURL(value);
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
default: {
|
|
2018
|
+
break;
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
return value;
|
|
2024
|
+
};
|
|
2025
|
+
|
|
2026
|
+
/**
|
|
2027
|
+
* Write a modified attribute value back onto the element. On
|
|
2028
|
+
* success, re-probe for clobbering introduced by the new value and
|
|
2029
|
+
* remove the element when found; otherwise pop the removal entry
|
|
2030
|
+
* recorded by the earlier _removeAttribute (long-standing pairing
|
|
2031
|
+
* with the SANITIZE_NAMED_PROPS path - do not "fix" casually). On
|
|
2032
|
+
* failure, remove the attribute instead.
|
|
2033
|
+
*
|
|
2034
|
+
* @param currentNode the element carrying the attribute
|
|
2035
|
+
* @param name the attribute name as present on the element
|
|
2036
|
+
* @param namespaceURI the attribute's namespace, if any
|
|
2037
|
+
* @param value the new attribute value
|
|
2038
|
+
*/
|
|
2039
|
+
const _setAttributeValue = function (
|
|
2040
|
+
currentNode: Element,
|
|
2041
|
+
name: string,
|
|
2042
|
+
namespaceURI: string | null,
|
|
2043
|
+
value: string
|
|
2044
|
+
): void {
|
|
2045
|
+
try {
|
|
2046
|
+
if (namespaceURI) {
|
|
2047
|
+
currentNode.setAttributeNS(namespaceURI, name, value);
|
|
2048
|
+
} else {
|
|
2049
|
+
/* Fallback to setAttribute() for browser-unrecognized namespaces e.g. "x-schema". */
|
|
2050
|
+
currentNode.setAttribute(name, value);
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
if (_isClobbered(currentNode)) {
|
|
2054
|
+
_forceRemove(currentNode);
|
|
2055
|
+
} else {
|
|
2056
|
+
arrayPop(DOMPurify.removed);
|
|
2057
|
+
}
|
|
2058
|
+
} catch (_) {
|
|
2059
|
+
_removeAttribute(name, currentNode);
|
|
2060
|
+
}
|
|
2061
|
+
};
|
|
2062
|
+
|
|
1613
2063
|
/**
|
|
1614
2064
|
* _sanitizeAttributes
|
|
1615
2065
|
*
|
|
@@ -1639,6 +2089,7 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
1639
2089
|
forceKeepAttr: undefined,
|
|
1640
2090
|
};
|
|
1641
2091
|
let l = attributes.length;
|
|
2092
|
+
const lcTag = transformCaseFunc(currentNode.nodeName);
|
|
1642
2093
|
|
|
1643
2094
|
/* Go backwards over all attributes; safely remove bad ones */
|
|
1644
2095
|
while (l--) {
|
|
@@ -1691,7 +2142,7 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
1691
2142
|
continue;
|
|
1692
2143
|
}
|
|
1693
2144
|
|
|
1694
|
-
/* Did the hooks
|
|
2145
|
+
/* Did the hooks force-keep the attribute? */
|
|
1695
2146
|
if (hookEvent.forceKeepAttr) {
|
|
1696
2147
|
continue;
|
|
1697
2148
|
}
|
|
@@ -1703,70 +2154,31 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
1703
2154
|
}
|
|
1704
2155
|
|
|
1705
2156
|
/* Work around a security issue in jQuery 3.0 */
|
|
1706
|
-
if (
|
|
2157
|
+
if (
|
|
2158
|
+
!ALLOW_SELF_CLOSE_IN_ATTR &&
|
|
2159
|
+
regExpTest(EXPRESSIONS.SELF_CLOSING_TAG, value)
|
|
2160
|
+
) {
|
|
1707
2161
|
_removeAttribute(name, currentNode);
|
|
1708
2162
|
continue;
|
|
1709
2163
|
}
|
|
1710
2164
|
|
|
1711
2165
|
/* Sanitize attribute content to be template-safe */
|
|
1712
2166
|
if (SAFE_FOR_TEMPLATES) {
|
|
1713
|
-
|
|
1714
|
-
value = stringReplace(value, expr, ' ');
|
|
1715
|
-
});
|
|
2167
|
+
value = _stripTemplateExpressions(value);
|
|
1716
2168
|
}
|
|
1717
2169
|
|
|
1718
2170
|
/* Is `value` valid for this attribute? */
|
|
1719
|
-
const lcTag = transformCaseFunc(currentNode.nodeName);
|
|
1720
2171
|
if (!_isValidAttribute(lcTag, lcName, value)) {
|
|
1721
2172
|
_removeAttribute(name, currentNode);
|
|
1722
2173
|
continue;
|
|
1723
2174
|
}
|
|
1724
2175
|
|
|
1725
2176
|
/* Handle attributes that require Trusted Types */
|
|
1726
|
-
|
|
1727
|
-
trustedTypesPolicy &&
|
|
1728
|
-
typeof trustedTypes === 'object' &&
|
|
1729
|
-
typeof trustedTypes.getAttributeType === 'function'
|
|
1730
|
-
) {
|
|
1731
|
-
if (namespaceURI) {
|
|
1732
|
-
/* Namespaces are not yet supported, see https://bugs.chromium.org/p/chromium/issues/detail?id=1305293 */
|
|
1733
|
-
} else {
|
|
1734
|
-
switch (trustedTypes.getAttributeType(lcTag, lcName)) {
|
|
1735
|
-
case 'TrustedHTML': {
|
|
1736
|
-
value = _createTrustedHTML(value);
|
|
1737
|
-
break;
|
|
1738
|
-
}
|
|
1739
|
-
|
|
1740
|
-
case 'TrustedScriptURL': {
|
|
1741
|
-
value = trustedTypesPolicy.createScriptURL(value);
|
|
1742
|
-
break;
|
|
1743
|
-
}
|
|
1744
|
-
|
|
1745
|
-
default: {
|
|
1746
|
-
break;
|
|
1747
|
-
}
|
|
1748
|
-
}
|
|
1749
|
-
}
|
|
1750
|
-
}
|
|
2177
|
+
value = _applyTrustedTypesToAttribute(lcTag, lcName, namespaceURI, value);
|
|
1751
2178
|
|
|
1752
2179
|
/* Handle invalid data-* attribute set by try-catching it */
|
|
1753
2180
|
if (value !== initValue) {
|
|
1754
|
-
|
|
1755
|
-
if (namespaceURI) {
|
|
1756
|
-
currentNode.setAttributeNS(namespaceURI, name, value);
|
|
1757
|
-
} else {
|
|
1758
|
-
/* Fallback to setAttribute() for browser-unrecognized namespaces e.g. "x-schema". */
|
|
1759
|
-
currentNode.setAttribute(name, value);
|
|
1760
|
-
}
|
|
1761
|
-
|
|
1762
|
-
if (_isClobbered(currentNode)) {
|
|
1763
|
-
_forceRemove(currentNode);
|
|
1764
|
-
} else {
|
|
1765
|
-
arrayPop(DOMPurify.removed);
|
|
1766
|
-
}
|
|
1767
|
-
} catch (_) {
|
|
1768
|
-
_removeAttribute(name, currentNode);
|
|
1769
|
-
}
|
|
2181
|
+
_setAttributeValue(currentNode, name, namespaceURI, value);
|
|
1770
2182
|
}
|
|
1771
2183
|
}
|
|
1772
2184
|
|
|
@@ -1818,9 +2230,7 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
1818
2230
|
? getNodeType(shadowNode)
|
|
1819
2231
|
: shadowNode.nodeType;
|
|
1820
2232
|
if (shadowNodeType === NODE_TYPE.element) {
|
|
1821
|
-
const innerSr = getShadowRoot
|
|
1822
|
-
? getShadowRoot(shadowNode)
|
|
1823
|
-
: (shadowNode as Element).shadowRoot;
|
|
2233
|
+
const innerSr = getShadowRoot(shadowNode);
|
|
1824
2234
|
if (_isDocumentFragment(innerSr)) {
|
|
1825
2235
|
_sanitizeAttachedShadowRoots(innerSr);
|
|
1826
2236
|
_sanitizeShadowDOM(innerSr);
|
|
@@ -1852,57 +2262,76 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
1852
2262
|
* @param root the subtree root to walk for attached shadow roots
|
|
1853
2263
|
*/
|
|
1854
2264
|
const _sanitizeAttachedShadowRoots = function (root: Node): void {
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
2265
|
+
/* Iterative (explicit stack) rather than per-child recursion. DOM APIs
|
|
2266
|
+
impose no depth cap, so an attacker-shaped tree (JSON/CRDT/editor data
|
|
2267
|
+
built straight into the DOM — the IN_PLACE surface) deeper than the JS
|
|
2268
|
+
call-stack budget would otherwise overflow native recursion here and
|
|
2269
|
+
throw at the IN_PLACE entry pre-pass, before a single node is
|
|
2270
|
+
sanitized, leaving the caller's live tree untouched (fail-open). See
|
|
2271
|
+
campaign-3 F4. A heap stack keeps depth off the call stack.
|
|
2272
|
+
|
|
2273
|
+
Each work item is either a node to descend into, or a deferred
|
|
2274
|
+
`_sanitizeShadowDOM` for an already-walked shadow root. The deferred
|
|
2275
|
+
form preserves the original post-order discipline: a shadow root's
|
|
2276
|
+
nested shadow roots are discovered before the outer shadow is
|
|
2277
|
+
sanitized (which may remove hosts). Pushes are in reverse of the
|
|
2278
|
+
desired processing order (LIFO): template content, then children, then
|
|
2279
|
+
the shadow-sanitize, then the shadow walk — so the order matches the
|
|
2280
|
+
previous recursion exactly. */
|
|
2281
|
+
const stack: Array<{ node: Node | null; shadow: DocumentFragment | null }> =
|
|
2282
|
+
[{ node: root, shadow: null }];
|
|
2283
|
+
|
|
2284
|
+
while (stack.length > 0) {
|
|
2285
|
+
const item = stack.pop();
|
|
2286
|
+
|
|
2287
|
+
/* Deferred shadow-DOM sanitisation: runs after its subtree was walked. */
|
|
2288
|
+
if (item.shadow) {
|
|
2289
|
+
_sanitizeShadowDOM(item.shadow);
|
|
2290
|
+
continue;
|
|
1873
2291
|
}
|
|
1874
|
-
}
|
|
1875
|
-
|
|
1876
|
-
// Snapshot children before recursing. Sanitization of one subtree
|
|
1877
|
-
// (e.g. via an uponSanitizeShadowNode hook) may detach siblings,
|
|
1878
|
-
// and naive nextSibling traversal would silently skip the rest of
|
|
1879
|
-
// the list once a node is detached.
|
|
1880
|
-
const childNodes = getChildNodes
|
|
1881
|
-
? getChildNodes(root)
|
|
1882
|
-
: (root as Element).childNodes;
|
|
1883
|
-
if (!childNodes) {
|
|
1884
|
-
return;
|
|
1885
|
-
}
|
|
1886
2292
|
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
2293
|
+
const node = item.node;
|
|
2294
|
+
const nodeType = getNodeType ? getNodeType(node) : (node as any).nodeType;
|
|
2295
|
+
const isElement = nodeType === NODE_TYPE.element;
|
|
2296
|
+
|
|
2297
|
+
/* (pushed last → processed first) Children, snapshotted in reverse so
|
|
2298
|
+
the first child is processed first. Snapshotting matters because a
|
|
2299
|
+
hook may detach siblings mid-walk. */
|
|
2300
|
+
const childNodes = getChildNodes(node);
|
|
2301
|
+
if (childNodes) {
|
|
2302
|
+
for (let i = childNodes.length - 1; i >= 0; --i) {
|
|
2303
|
+
stack.push({ node: childNodes[i], shadow: null });
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
1891
2306
|
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
2307
|
+
/* (pushed before children → processed after them, matching the old
|
|
2308
|
+
"template content last" order) When the node is a <template>,
|
|
2309
|
+
descend into its content. */
|
|
2310
|
+
if (isElement) {
|
|
2311
|
+
const rootName = getNodeName ? getNodeName(node) : null;
|
|
2312
|
+
if (
|
|
2313
|
+
typeof rootName === 'string' &&
|
|
2314
|
+
transformCaseFunc(rootName) === 'template'
|
|
2315
|
+
) {
|
|
2316
|
+
const content = (node as HTMLTemplateElement).content;
|
|
2317
|
+
if (_isDocumentFragment(content)) {
|
|
2318
|
+
stack.push({ node: content, shadow: null });
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
1895
2322
|
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
2323
|
+
/* Shadow root (processed first): walk its subtree, then sanitise it.
|
|
2324
|
+
Realm-safe check (GHSA-hpcv-96wg-7vj8): nodeType-based detection
|
|
2325
|
+
rather than `instanceof DocumentFragment`, which is realm-bound and
|
|
2326
|
+
silently skipped foreign-realm shadow roots (e.g.
|
|
2327
|
+
iframe.contentDocument attachShadow). */
|
|
2328
|
+
if (isElement) {
|
|
2329
|
+
const sr = getShadowRoot(node);
|
|
2330
|
+
if (_isDocumentFragment(sr)) {
|
|
2331
|
+
/* Push the deferred sanitise first so it pops after the shadow
|
|
2332
|
+
walk we push next, i.e. nested shadow roots are discovered
|
|
2333
|
+
before this one is sanitised. */
|
|
2334
|
+
stack.push({ node: null, shadow: sr }, { node: sr, shadow: null });
|
|
1906
2335
|
}
|
|
1907
2336
|
}
|
|
1908
2337
|
}
|
|
@@ -1944,12 +2373,15 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
1944
2373
|
/* Clean up removed elements */
|
|
1945
2374
|
DOMPurify.removed = [];
|
|
1946
2375
|
|
|
1947
|
-
/*
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
2376
|
+
/* Resolve IN_PLACE for this call without mutating persistent config.
|
|
2377
|
+
Writing the IN_PLACE closure variable here leaks under setConfig(),
|
|
2378
|
+
where _parseConfig is skipped on later calls: a single string call would
|
|
2379
|
+
disable in-place mode for every subsequent node call, returning a
|
|
2380
|
+
sanitized copy while leaving the caller's node — which in-place callers
|
|
2381
|
+
keep using and whose return value they ignore — unsanitized. REPORT-2. */
|
|
2382
|
+
const inPlace = IN_PLACE && typeof dirty !== 'string' && _isNode(dirty);
|
|
1951
2383
|
|
|
1952
|
-
if (
|
|
2384
|
+
if (inPlace) {
|
|
1953
2385
|
/* Do some early pre-sanitization to avoid unsafe root nodes.
|
|
1954
2386
|
Read nodeName through the cached prototype getter — a clobbering
|
|
1955
2387
|
child named "nodeName" on the form root would otherwise shadow
|
|
@@ -1984,8 +2416,17 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
1984
2416
|
}
|
|
1985
2417
|
|
|
1986
2418
|
/* Sanitize attached shadow roots before the main iterator runs.
|
|
1987
|
-
The iterator does not descend into shadow trees.
|
|
1988
|
-
|
|
2419
|
+
The iterator does not descend into shadow trees. Same fail-closed
|
|
2420
|
+
barrier as the main walk (campaign-3 F2): a custom-element reaction
|
|
2421
|
+
inside a shadow root could abort this pre-pass before the walk runs,
|
|
2422
|
+
which would otherwise leave the entire live tree unsanitized. */
|
|
2423
|
+
try {
|
|
2424
|
+
_sanitizeAttachedShadowRoots(dirty as Node);
|
|
2425
|
+
} catch (error) {
|
|
2426
|
+
_neutralizeRoot(dirty as Node);
|
|
2427
|
+
|
|
2428
|
+
throw error;
|
|
2429
|
+
}
|
|
1989
2430
|
} else if (_isNode(dirty)) {
|
|
1990
2431
|
/* If dirty is a DOM element, append to an empty document to avoid
|
|
1991
2432
|
elements being stripped by the parser */
|
|
@@ -2039,27 +2480,56 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
2039
2480
|
}
|
|
2040
2481
|
|
|
2041
2482
|
/* Get node iterator */
|
|
2042
|
-
const nodeIterator = _createNodeIterator(
|
|
2043
|
-
|
|
2044
|
-
/* Now start iterating over the created document
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2483
|
+
const nodeIterator = _createNodeIterator(inPlace ? dirty : body);
|
|
2484
|
+
|
|
2485
|
+
/* Now start iterating over the created document.
|
|
2486
|
+
The walk runs inside an exception barrier (campaign-3 F2): a re-entrant
|
|
2487
|
+
engine/custom-element mutation can detach a node mid-walk so
|
|
2488
|
+
`_forceRemove`'s parentless guard throws, aborting the loop. Without the
|
|
2489
|
+
barrier the caller's in-place tree would be left half-sanitized with the
|
|
2490
|
+
unvisited tail still armed. On any throw we fail closed — strip the
|
|
2491
|
+
in-place root bare — then rethrow so the existing throw contract is
|
|
2492
|
+
preserved. (String/DOM-copy paths never return the partial body, so the
|
|
2493
|
+
propagating throw is already fail-closed there.) */
|
|
2494
|
+
try {
|
|
2495
|
+
while ((currentNode = nodeIterator.nextNode())) {
|
|
2496
|
+
/* Sanitize tags and elements */
|
|
2497
|
+
_sanitizeElements(currentNode);
|
|
2498
|
+
|
|
2499
|
+
/* Check attributes next */
|
|
2500
|
+
_sanitizeAttributes(currentNode);
|
|
2501
|
+
|
|
2502
|
+
/* Shadow DOM detected, sanitize it.
|
|
2503
|
+
Realm-safe check (GHSA-hpcv-96wg-7vj8): nodeType-based detection
|
|
2504
|
+
instead of instanceof, so foreign-realm <template>.content is
|
|
2505
|
+
walked correctly. */
|
|
2506
|
+
if (_isDocumentFragment(currentNode.content)) {
|
|
2507
|
+
_sanitizeShadowDOM(currentNode.content);
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
} catch (error) {
|
|
2511
|
+
if (inPlace) {
|
|
2512
|
+
_neutralizeRoot(dirty as Node);
|
|
2058
2513
|
}
|
|
2514
|
+
|
|
2515
|
+
throw error;
|
|
2059
2516
|
}
|
|
2060
2517
|
|
|
2061
2518
|
/* If we sanitized `dirty` in-place, return it. */
|
|
2062
|
-
if (
|
|
2519
|
+
if (inPlace) {
|
|
2520
|
+
/* Fail-closed completion of the audit-5 F1 fix: every node removed from
|
|
2521
|
+
the caller's live tree is detached but may still hold a queued
|
|
2522
|
+
resource-event handler that fires in page scope after we return. The
|
|
2523
|
+
move-hoist covers only disallowed-tag KEEP_CONTENT removals; strip the
|
|
2524
|
+
non-allow-listed attributes off every other removed subtree (clobber,
|
|
2525
|
+
mXSS, namespace, comments, KEEP_CONTENT:false, …) so those handlers are
|
|
2526
|
+
cancelled before any event can fire. Runs synchronously, pre-return. */
|
|
2527
|
+
arrayForEach(DOMPurify.removed, (entry) => {
|
|
2528
|
+
if (entry.element) {
|
|
2529
|
+
_neutralizeSubtree(entry.element as Node);
|
|
2530
|
+
}
|
|
2531
|
+
});
|
|
2532
|
+
|
|
2063
2533
|
if (SAFE_FOR_TEMPLATES) {
|
|
2064
2534
|
_scrubTemplateExpressions(dirty as Element);
|
|
2065
2535
|
}
|
|
@@ -2115,9 +2585,7 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
2115
2585
|
|
|
2116
2586
|
/* Sanitize final string template-safe */
|
|
2117
2587
|
if (SAFE_FOR_TEMPLATES) {
|
|
2118
|
-
|
|
2119
|
-
serializedHTML = stringReplace(serializedHTML, expr, ' ');
|
|
2120
|
-
});
|
|
2588
|
+
serializedHTML = _stripTemplateExpressions(serializedHTML);
|
|
2121
2589
|
}
|
|
2122
2590
|
|
|
2123
2591
|
return trustedTypesPolicy && RETURN_TRUSTED_TYPE
|
|
@@ -2133,6 +2601,13 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
2133
2601
|
DOMPurify.clearConfig = function () {
|
|
2134
2602
|
CONFIG = null;
|
|
2135
2603
|
SET_CONFIG = false;
|
|
2604
|
+
|
|
2605
|
+
// Drop any caller-supplied Trusted Types policy so it cannot poison later
|
|
2606
|
+
// `RETURN_TRUSTED_TYPE` output. The internal default policy (cached, and
|
|
2607
|
+
// never recreated — Trusted Types throws on duplicate names) is restored by
|
|
2608
|
+
// the next `_parseConfig`. See GHSA-vxr8-fq34-vvx9.
|
|
2609
|
+
trustedTypesPolicy = defaultTrustedTypesPolicy;
|
|
2610
|
+
emptyHTML = '';
|
|
2136
2611
|
};
|
|
2137
2612
|
|
|
2138
2613
|
DOMPurify.isValidAttribute = function (tag, attr, value) {
|
|
@@ -2184,352 +2659,3 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
|
|
|
2184
2659
|
}
|
|
2185
2660
|
|
|
2186
2661
|
export default createDOMPurify();
|
|
2187
|
-
|
|
2188
|
-
export interface DOMPurify {
|
|
2189
|
-
/**
|
|
2190
|
-
* Creates a DOMPurify instance using the given window-like object. Defaults to `window`.
|
|
2191
|
-
*/
|
|
2192
|
-
(root?: WindowLike): DOMPurify;
|
|
2193
|
-
|
|
2194
|
-
/**
|
|
2195
|
-
* Version label, exposed for easier checks
|
|
2196
|
-
* if DOMPurify is up to date or not
|
|
2197
|
-
*/
|
|
2198
|
-
version: string;
|
|
2199
|
-
|
|
2200
|
-
/**
|
|
2201
|
-
* Array of elements that DOMPurify removed during sanitation.
|
|
2202
|
-
* Empty if nothing was removed.
|
|
2203
|
-
*/
|
|
2204
|
-
removed: Array<RemovedElement | RemovedAttribute>;
|
|
2205
|
-
|
|
2206
|
-
/**
|
|
2207
|
-
* Expose whether this browser supports running the full DOMPurify.
|
|
2208
|
-
*/
|
|
2209
|
-
isSupported: boolean;
|
|
2210
|
-
|
|
2211
|
-
/**
|
|
2212
|
-
* Set the configuration once.
|
|
2213
|
-
*
|
|
2214
|
-
* @param cfg configuration object
|
|
2215
|
-
*/
|
|
2216
|
-
setConfig(cfg?: Config): void;
|
|
2217
|
-
|
|
2218
|
-
/**
|
|
2219
|
-
* Removes the configuration.
|
|
2220
|
-
*/
|
|
2221
|
-
clearConfig(): void;
|
|
2222
|
-
|
|
2223
|
-
/**
|
|
2224
|
-
* Provides core sanitation functionality.
|
|
2225
|
-
*
|
|
2226
|
-
* @param dirty string or DOM node
|
|
2227
|
-
* @param cfg object
|
|
2228
|
-
* @returns Sanitized TrustedHTML.
|
|
2229
|
-
*/
|
|
2230
|
-
sanitize(
|
|
2231
|
-
dirty: string | Node,
|
|
2232
|
-
cfg: Config & { RETURN_TRUSTED_TYPE: true }
|
|
2233
|
-
): TrustedHTML;
|
|
2234
|
-
|
|
2235
|
-
/**
|
|
2236
|
-
* Provides core sanitation functionality.
|
|
2237
|
-
*
|
|
2238
|
-
* @param dirty DOM node
|
|
2239
|
-
* @param cfg object
|
|
2240
|
-
* @returns Sanitized DOM node.
|
|
2241
|
-
*/
|
|
2242
|
-
sanitize(dirty: Node, cfg: Config & { IN_PLACE: true }): Node;
|
|
2243
|
-
|
|
2244
|
-
/**
|
|
2245
|
-
* Provides core sanitation functionality.
|
|
2246
|
-
*
|
|
2247
|
-
* @param dirty string or DOM node
|
|
2248
|
-
* @param cfg object
|
|
2249
|
-
* @returns Sanitized DOM node.
|
|
2250
|
-
*/
|
|
2251
|
-
sanitize(dirty: string | Node, cfg: Config & { RETURN_DOM: true }): Node;
|
|
2252
|
-
|
|
2253
|
-
/**
|
|
2254
|
-
* Provides core sanitation functionality.
|
|
2255
|
-
*
|
|
2256
|
-
* @param dirty string or DOM node
|
|
2257
|
-
* @param cfg object
|
|
2258
|
-
* @returns Sanitized document fragment.
|
|
2259
|
-
*/
|
|
2260
|
-
sanitize(
|
|
2261
|
-
dirty: string | Node,
|
|
2262
|
-
cfg: Config & { RETURN_DOM_FRAGMENT: true }
|
|
2263
|
-
): DocumentFragment;
|
|
2264
|
-
|
|
2265
|
-
/**
|
|
2266
|
-
* Provides core sanitation functionality.
|
|
2267
|
-
*
|
|
2268
|
-
* @param dirty string or DOM node
|
|
2269
|
-
* @param cfg object
|
|
2270
|
-
* @returns Sanitized string.
|
|
2271
|
-
*/
|
|
2272
|
-
sanitize(dirty: string | Node, cfg?: Config): string;
|
|
2273
|
-
|
|
2274
|
-
/**
|
|
2275
|
-
* Checks if an attribute value is valid.
|
|
2276
|
-
* Uses last set config, if any. Otherwise, uses config defaults.
|
|
2277
|
-
*
|
|
2278
|
-
* @param tag Tag name of containing element.
|
|
2279
|
-
* @param attr Attribute name.
|
|
2280
|
-
* @param value Attribute value.
|
|
2281
|
-
* @returns Returns true if `value` is valid. Otherwise, returns false.
|
|
2282
|
-
*/
|
|
2283
|
-
isValidAttribute(tag: string, attr: string, value: string): boolean;
|
|
2284
|
-
|
|
2285
|
-
/**
|
|
2286
|
-
* Adds a DOMPurify hook.
|
|
2287
|
-
*
|
|
2288
|
-
* @param entryPoint entry point for the hook to add
|
|
2289
|
-
* @param hookFunction function to execute
|
|
2290
|
-
*/
|
|
2291
|
-
addHook(entryPoint: BasicHookName, hookFunction: NodeHook): void;
|
|
2292
|
-
|
|
2293
|
-
/**
|
|
2294
|
-
* Adds a DOMPurify hook.
|
|
2295
|
-
*
|
|
2296
|
-
* @param entryPoint entry point for the hook to add
|
|
2297
|
-
* @param hookFunction function to execute
|
|
2298
|
-
*/
|
|
2299
|
-
addHook(entryPoint: ElementHookName, hookFunction: ElementHook): void;
|
|
2300
|
-
|
|
2301
|
-
/**
|
|
2302
|
-
* Adds a DOMPurify hook.
|
|
2303
|
-
*
|
|
2304
|
-
* @param entryPoint entry point for the hook to add
|
|
2305
|
-
* @param hookFunction function to execute
|
|
2306
|
-
*/
|
|
2307
|
-
addHook(
|
|
2308
|
-
entryPoint: DocumentFragmentHookName,
|
|
2309
|
-
hookFunction: DocumentFragmentHook
|
|
2310
|
-
): void;
|
|
2311
|
-
|
|
2312
|
-
/**
|
|
2313
|
-
* Adds a DOMPurify hook.
|
|
2314
|
-
*
|
|
2315
|
-
* @param entryPoint entry point for the hook to add
|
|
2316
|
-
* @param hookFunction function to execute
|
|
2317
|
-
*/
|
|
2318
|
-
addHook(
|
|
2319
|
-
entryPoint: 'uponSanitizeElement',
|
|
2320
|
-
hookFunction: UponSanitizeElementHook
|
|
2321
|
-
): void;
|
|
2322
|
-
|
|
2323
|
-
/**
|
|
2324
|
-
* Adds a DOMPurify hook.
|
|
2325
|
-
*
|
|
2326
|
-
* @param entryPoint entry point for the hook to add
|
|
2327
|
-
* @param hookFunction function to execute
|
|
2328
|
-
*/
|
|
2329
|
-
addHook(
|
|
2330
|
-
entryPoint: 'uponSanitizeAttribute',
|
|
2331
|
-
hookFunction: UponSanitizeAttributeHook
|
|
2332
|
-
): void;
|
|
2333
|
-
|
|
2334
|
-
/**
|
|
2335
|
-
* Remove a DOMPurify hook at a given entryPoint
|
|
2336
|
-
* (pops it from the stack of hooks if hook not specified)
|
|
2337
|
-
*
|
|
2338
|
-
* @param entryPoint entry point for the hook to remove
|
|
2339
|
-
* @param hookFunction optional specific hook to remove
|
|
2340
|
-
* @returns removed hook
|
|
2341
|
-
*/
|
|
2342
|
-
removeHook(
|
|
2343
|
-
entryPoint: BasicHookName,
|
|
2344
|
-
hookFunction?: NodeHook
|
|
2345
|
-
): NodeHook | undefined;
|
|
2346
|
-
|
|
2347
|
-
/**
|
|
2348
|
-
* Remove a DOMPurify hook at a given entryPoint
|
|
2349
|
-
* (pops it from the stack of hooks if hook not specified)
|
|
2350
|
-
*
|
|
2351
|
-
* @param entryPoint entry point for the hook to remove
|
|
2352
|
-
* @param hookFunction optional specific hook to remove
|
|
2353
|
-
* @returns removed hook
|
|
2354
|
-
*/
|
|
2355
|
-
removeHook(
|
|
2356
|
-
entryPoint: ElementHookName,
|
|
2357
|
-
hookFunction?: ElementHook
|
|
2358
|
-
): ElementHook | undefined;
|
|
2359
|
-
|
|
2360
|
-
/**
|
|
2361
|
-
* Remove a DOMPurify hook at a given entryPoint
|
|
2362
|
-
* (pops it from the stack of hooks if hook not specified)
|
|
2363
|
-
*
|
|
2364
|
-
* @param entryPoint entry point for the hook to remove
|
|
2365
|
-
* @param hookFunction optional specific hook to remove
|
|
2366
|
-
* @returns removed hook
|
|
2367
|
-
*/
|
|
2368
|
-
removeHook(
|
|
2369
|
-
entryPoint: DocumentFragmentHookName,
|
|
2370
|
-
hookFunction?: DocumentFragmentHook
|
|
2371
|
-
): DocumentFragmentHook | undefined;
|
|
2372
|
-
|
|
2373
|
-
/**
|
|
2374
|
-
* Remove a DOMPurify hook at a given entryPoint
|
|
2375
|
-
* (pops it from the stack of hooks if hook not specified)
|
|
2376
|
-
*
|
|
2377
|
-
* @param entryPoint entry point for the hook to remove
|
|
2378
|
-
* @param hookFunction optional specific hook to remove
|
|
2379
|
-
* @returns removed hook
|
|
2380
|
-
*/
|
|
2381
|
-
removeHook(
|
|
2382
|
-
entryPoint: 'uponSanitizeElement',
|
|
2383
|
-
hookFunction?: UponSanitizeElementHook
|
|
2384
|
-
): UponSanitizeElementHook | undefined;
|
|
2385
|
-
|
|
2386
|
-
/**
|
|
2387
|
-
* Remove a DOMPurify hook at a given entryPoint
|
|
2388
|
-
* (pops it from the stack of hooks if hook not specified)
|
|
2389
|
-
*
|
|
2390
|
-
* @param entryPoint entry point for the hook to remove
|
|
2391
|
-
* @param hookFunction optional specific hook to remove
|
|
2392
|
-
* @returns removed hook
|
|
2393
|
-
*/
|
|
2394
|
-
removeHook(
|
|
2395
|
-
entryPoint: 'uponSanitizeAttribute',
|
|
2396
|
-
hookFunction?: UponSanitizeAttributeHook
|
|
2397
|
-
): UponSanitizeAttributeHook | undefined;
|
|
2398
|
-
|
|
2399
|
-
/**
|
|
2400
|
-
* Removes all DOMPurify hooks at a given entryPoint
|
|
2401
|
-
*
|
|
2402
|
-
* @param entryPoint entry point for the hooks to remove
|
|
2403
|
-
*/
|
|
2404
|
-
removeHooks(entryPoint: HookName): void;
|
|
2405
|
-
|
|
2406
|
-
/**
|
|
2407
|
-
* Removes all DOMPurify hooks.
|
|
2408
|
-
*/
|
|
2409
|
-
removeAllHooks(): void;
|
|
2410
|
-
}
|
|
2411
|
-
|
|
2412
|
-
/**
|
|
2413
|
-
* An element removed by DOMPurify.
|
|
2414
|
-
*/
|
|
2415
|
-
export interface RemovedElement {
|
|
2416
|
-
/**
|
|
2417
|
-
* The element that was removed.
|
|
2418
|
-
*/
|
|
2419
|
-
element: Node;
|
|
2420
|
-
}
|
|
2421
|
-
|
|
2422
|
-
/**
|
|
2423
|
-
* An element removed by DOMPurify.
|
|
2424
|
-
*/
|
|
2425
|
-
export interface RemovedAttribute {
|
|
2426
|
-
/**
|
|
2427
|
-
* The attribute that was removed.
|
|
2428
|
-
*/
|
|
2429
|
-
attribute: Attr | null;
|
|
2430
|
-
|
|
2431
|
-
/**
|
|
2432
|
-
* The element that the attribute was removed.
|
|
2433
|
-
*/
|
|
2434
|
-
from: Node;
|
|
2435
|
-
}
|
|
2436
|
-
|
|
2437
|
-
type BasicHookName =
|
|
2438
|
-
| 'beforeSanitizeElements'
|
|
2439
|
-
| 'afterSanitizeElements'
|
|
2440
|
-
| 'uponSanitizeShadowNode';
|
|
2441
|
-
type ElementHookName = 'beforeSanitizeAttributes' | 'afterSanitizeAttributes';
|
|
2442
|
-
type DocumentFragmentHookName =
|
|
2443
|
-
| 'beforeSanitizeShadowDOM'
|
|
2444
|
-
| 'afterSanitizeShadowDOM';
|
|
2445
|
-
type UponSanitizeElementHookName = 'uponSanitizeElement';
|
|
2446
|
-
type UponSanitizeAttributeHookName = 'uponSanitizeAttribute';
|
|
2447
|
-
|
|
2448
|
-
interface HooksMap {
|
|
2449
|
-
beforeSanitizeElements: NodeHook[];
|
|
2450
|
-
afterSanitizeElements: NodeHook[];
|
|
2451
|
-
beforeSanitizeShadowDOM: DocumentFragmentHook[];
|
|
2452
|
-
uponSanitizeShadowNode: NodeHook[];
|
|
2453
|
-
afterSanitizeShadowDOM: DocumentFragmentHook[];
|
|
2454
|
-
beforeSanitizeAttributes: ElementHook[];
|
|
2455
|
-
afterSanitizeAttributes: ElementHook[];
|
|
2456
|
-
uponSanitizeElement: UponSanitizeElementHook[];
|
|
2457
|
-
uponSanitizeAttribute: UponSanitizeAttributeHook[];
|
|
2458
|
-
}
|
|
2459
|
-
|
|
2460
|
-
type ArrayElement<T> = T extends Array<infer U> ? U : never;
|
|
2461
|
-
|
|
2462
|
-
type HookFunction = ArrayElement<HooksMap[keyof HooksMap]>;
|
|
2463
|
-
|
|
2464
|
-
export type HookName =
|
|
2465
|
-
| BasicHookName
|
|
2466
|
-
| ElementHookName
|
|
2467
|
-
| DocumentFragmentHookName
|
|
2468
|
-
| UponSanitizeElementHookName
|
|
2469
|
-
| UponSanitizeAttributeHookName;
|
|
2470
|
-
|
|
2471
|
-
export type NodeHook = (
|
|
2472
|
-
this: DOMPurify,
|
|
2473
|
-
currentNode: Node,
|
|
2474
|
-
hookEvent: null,
|
|
2475
|
-
config: Config
|
|
2476
|
-
) => void;
|
|
2477
|
-
|
|
2478
|
-
export type ElementHook = (
|
|
2479
|
-
this: DOMPurify,
|
|
2480
|
-
currentNode: Element,
|
|
2481
|
-
hookEvent: null,
|
|
2482
|
-
config: Config
|
|
2483
|
-
) => void;
|
|
2484
|
-
|
|
2485
|
-
export type DocumentFragmentHook = (
|
|
2486
|
-
this: DOMPurify,
|
|
2487
|
-
currentNode: DocumentFragment,
|
|
2488
|
-
hookEvent: null,
|
|
2489
|
-
config: Config
|
|
2490
|
-
) => void;
|
|
2491
|
-
|
|
2492
|
-
export type UponSanitizeElementHook = (
|
|
2493
|
-
this: DOMPurify,
|
|
2494
|
-
currentNode: Node,
|
|
2495
|
-
hookEvent: UponSanitizeElementHookEvent,
|
|
2496
|
-
config: Config
|
|
2497
|
-
) => void;
|
|
2498
|
-
|
|
2499
|
-
export type UponSanitizeAttributeHook = (
|
|
2500
|
-
this: DOMPurify,
|
|
2501
|
-
currentNode: Element,
|
|
2502
|
-
hookEvent: UponSanitizeAttributeHookEvent,
|
|
2503
|
-
config: Config
|
|
2504
|
-
) => void;
|
|
2505
|
-
|
|
2506
|
-
export interface UponSanitizeElementHookEvent {
|
|
2507
|
-
tagName: string;
|
|
2508
|
-
allowedTags: Record<string, boolean>;
|
|
2509
|
-
}
|
|
2510
|
-
|
|
2511
|
-
export interface UponSanitizeAttributeHookEvent {
|
|
2512
|
-
attrName: string;
|
|
2513
|
-
attrValue: string;
|
|
2514
|
-
keepAttr: boolean;
|
|
2515
|
-
allowedAttributes: Record<string, boolean>;
|
|
2516
|
-
forceKeepAttr: boolean | undefined;
|
|
2517
|
-
}
|
|
2518
|
-
|
|
2519
|
-
/**
|
|
2520
|
-
* A `Window`-like object containing the properties and types that DOMPurify requires.
|
|
2521
|
-
*/
|
|
2522
|
-
export type WindowLike = Pick<
|
|
2523
|
-
typeof globalThis,
|
|
2524
|
-
| 'DocumentFragment'
|
|
2525
|
-
| 'HTMLTemplateElement'
|
|
2526
|
-
| 'Node'
|
|
2527
|
-
| 'Element'
|
|
2528
|
-
| 'NodeFilter'
|
|
2529
|
-
| 'NamedNodeMap'
|
|
2530
|
-
| 'HTMLFormElement'
|
|
2531
|
-
| 'DOMParser'
|
|
2532
|
-
> & {
|
|
2533
|
-
document?: Document;
|
|
2534
|
-
MozNamedAttrMap?: typeof window.NamedNodeMap;
|
|
2535
|
-
} & Pick<TrustedTypesWindow, 'trustedTypes'>;
|