import type { Config, UseProfilesConfig } from './config';
import type { DOMPurify, HooksMap, HookFunction, WindowLike } from './types';
import * as TAGS from './tags.js';
import * as ATTRS from './attrs.js';
import * as EXPRESSIONS from './regexp.js';
import {
addToSet,
clone,
entries,
freeze,
seal,
arrayForEach,
arrayIsArray,
arrayLastIndexOf,
arrayPop,
arrayPush,
arraySlice,
arraySplice,
stringMatch,
stringReplace,
stringToLowerCase,
stringToString,
stringIndexOf,
stringTrim,
regExpTest,
setAdd,
setCreate,
setHas,
isRegex,
typeErrorCreate,
lookupAccessor,
lookupGetter,
create,
objectHasOwnProperty,
stringifyValue,
} from './utils.js';
export type { Config } from './config';
export type {
DOMPurify,
RemovedElement,
RemovedAttribute,
HookName,
NodeHook,
ElementHook,
DocumentFragmentHook,
UponSanitizeElementHook,
UponSanitizeAttributeHook,
UponSanitizeElementHookEvent,
UponSanitizeAttributeHookEvent,
WindowLike,
} from './types';
declare const VERSION: string;
// https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
const NODE_TYPE = {
element: 1,
attribute: 2,
text: 3,
cdataSection: 4,
entityReference: 5, // Deprecated
entityNode: 6, // Deprecated
processingInstruction: 7,
comment: 8,
document: 9,
documentType: 10,
documentFragment: 11,
notation: 12, // Deprecated
};
/* HTML-namespace elements whose child text nodes are serialized *literally*
(unescaped) by the HTML fragment-serialization algorithm. Two reparse-mXSS
shapes ride on that literal serialization:
(a) an element child - a tree the HTML parser can never build, but the DOM
API and an XML/XHTML parse can - after which a ``-bearing text
sibling breaks the element open on reparse; and
(b) text-only content that already carries the element's OWN end tag, e.g.
`` built as a node, which the literal
serializer emits verbatim for the HTML parser to re-open.
Shape (a) is handled by the firstElementChild branch in _isUnsafeNode; shape
(b), and any other markup in the text, by the ELEMENT_MARKUP_PROBE branch next
to it. Both read textContent (the raw-serialized form for these elements)
rather than innerHTML, because an XML/XHTML working document serializes
innerHTML with `<` escaped, which silently blinds the innerHTML-based probes
(rule 1's second probe and FALLBACK_TAG_CLOSE) there.
`script` is never allow-listed, but is kept here so the guard matches the
serializer's own literal-text list exactly. */
const LITERAL_TEXT_ELEMENT_NAMES = [
'style',
'script',
'xmp',
'iframe',
'noembed',
'noframes',
'plaintext',
'noscript',
];
const LITERAL_TEXT_ELEMENTS = freeze(addToSet({}, LITERAL_TEXT_ELEMENT_NAMES));
const getGlobal = function (): WindowLike {
return typeof window === 'undefined' ? null : window;
};
/**
* Creates a no-op policy for internal use only.
* Don't export this function outside this module!
* @param trustedTypes The policy factory.
* @param purifyHostElement The Script element used to load DOMPurify (to determine policy name suffix).
* @return The policy created (or null, if Trusted Types
* are not supported or creating the policy failed).
*/
const _createTrustedTypesPolicy = function (
trustedTypes: TrustedTypePolicyFactory,
purifyHostElement: HTMLScriptElement
) {
if (
typeof trustedTypes !== 'object' ||
typeof trustedTypes.createPolicy !== 'function'
) {
return null;
}
// Allow the callers to control the unique policy name
// by adding a data-tt-policy-suffix to the script element with the DOMPurify.
// Policy creation with duplicate names throws in Trusted Types.
let suffix = null;
const ATTR_NAME = 'data-tt-policy-suffix';
if (purifyHostElement && purifyHostElement.hasAttribute(ATTR_NAME)) {
suffix = purifyHostElement.getAttribute(ATTR_NAME);
}
const policyName = 'dompurify' + (suffix ? '#' + suffix : '');
try {
return trustedTypes.createPolicy(policyName, {
createHTML(html) {
return html;
},
createScriptURL(scriptUrl) {
return scriptUrl;
},
});
} catch (_) {
// Policy creation failed (most likely another DOMPurify script has
// already run). Skip creating the policy, as this will only cause errors
// if TT are enforced.
console.warn(
'TrustedTypes policy ' + policyName + ' could not be created.'
);
return null;
}
};
const _createHooksMap = function (): HooksMap {
return {
afterSanitizeAttributes: [],
afterSanitizeElements: [],
afterSanitizeShadowDOM: [],
beforeSanitizeAttributes: [],
beforeSanitizeElements: [],
beforeSanitizeShadowDOM: [],
uponSanitizeAttribute: [],
uponSanitizeElement: [],
uponSanitizeShadowNode: [],
};
};
/**
* Resolve a set-valued configuration option: a fresh set built from
* cfg[key] when it is an own array property (seeded with a clone of
* options.base when given, case-normalized via options.transform),
* the fallback set otherwise.
*
* @param cfg the cloned, prototype-free configuration object
* @param key the configuration property to read
* @param fallback the set to use when the option is absent or not an array
* @param options transform and optional base set to merge into
* @returns the resolved set
*/
const _resolveSetOption = function (
cfg: Config,
key: keyof Config,
fallback: Record,
options: {
transform: Parameters[2];
base?: Record;
}
): Record {
return objectHasOwnProperty(cfg, key) && arrayIsArray(cfg[key])
? addToSet(
options.base ? clone(options.base) : {},
cfg[key] as readonly unknown[],
options.transform
)
: fallback;
};
/**
* Resolve an object-valued configuration option: a prototype-free clone
* of cfg[key] when it is an own, truthy object property, else a fresh
* fallback built by makeFallback (fresh on every parse, so a previous
* parse can never leak state into the next one).
*
* @param cfg the cloned, prototype-free configuration object
* @param key the configuration property to read
* @param makeFallback builds the fallback value when the option is absent
* @returns the resolved object
*/
const _resolveObjectOption = function >(
cfg: Config,
key: keyof Config,
makeFallback: () => T
): T {
const value = objectHasOwnProperty(cfg, key) ? cfg[key] : undefined;
return value && typeof value === 'object'
? clone(value as T)
: makeFallback();
};
function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
const DOMPurify: DOMPurify = (root: WindowLike) => createDOMPurify(root);
DOMPurify.version = VERSION;
DOMPurify.removed = [];
if (
!window ||
!window.document ||
window.document.nodeType !== NODE_TYPE.document ||
!window.Element
) {
// Not running in a browser, provide a factory function
// so that you can pass your own Window
DOMPurify.isSupported = false;
return DOMPurify;
}
let { document } = window;
const originalDocument = document;
const currentScript: HTMLScriptElement =
originalDocument.currentScript as HTMLScriptElement;
const {
DocumentFragment,
HTMLTemplateElement,
Node,
Element,
NodeFilter,
NamedNodeMap = window.NamedNodeMap || (window as any).MozNamedAttrMap,
HTMLFormElement,
DOMParser,
trustedTypes,
} = window;
const ElementPrototype = Element.prototype;
const cloneNode = lookupGetter(ElementPrototype, 'cloneNode');
const remove = lookupGetter(ElementPrototype, 'remove');
// Clobber-safe Attr-node removal. On an HTMLFormElement a descendant named
// "removeAttributeNode" shadows the prototype method via
// [LegacyOverrideBuiltIns], so element.removeAttributeNode(attr) throws.
// Calling the cached Element.prototype method with the element as the
// receiver removes the exact live Attr node regardless of the shadowing.
const removeAttributeNode = lookupGetter(
ElementPrototype,
'removeAttributeNode'
);
const getNextSibling = lookupGetter(ElementPrototype, 'nextSibling');
const getChildNodes = lookupGetter(ElementPrototype, 'childNodes');
const getParentNode = lookupGetter(ElementPrototype, 'parentNode');
const getShadowRoot = lookupGetter(ElementPrototype, 'shadowRoot');
const getAttributes = lookupGetter(ElementPrototype, 'attributes');
const getNodeType =
Node && Node.prototype ? lookupGetter(Node.prototype, 'nodeType') : null;
const getNodeName =
Node && Node.prototype ? lookupGetter(Node.prototype, 'nodeName') : null;
const getOwnerDocument =
Node && Node.prototype
? lookupGetter(Node.prototype, 'ownerDocument')
: null;
/* Clobber-safe nodeType / nodeName reads through the cached Node.prototype
getters, with a direct-property fallback for environments that lack
Node.prototype. Sites that need a different fallback (e.g. _isClobbered
returns early on a null name) intentionally keep their own reads. */
const _readNodeType = function (node: Node): number {
return getNodeType ? getNodeType(node) : (node as any).nodeType;
};
const _readNodeName = function (node: Node): string {
return getNodeName ? getNodeName(node) : (node as any).nodeName;
};
/* Element namespace through the cached Element.prototype getter, so a live
node's own-property override (Object.defineProperty) cannot steer
namespace decisions; direct read where the getter does not exist. Only
call this on Elements - the getter throws on other node types. */
const getNamespaceURI = lookupAccessor(ElementPrototype, 'namespaceURI');
const _readNamespaceURI = function (element: Element): string | null {
return getNamespaceURI
? getNamespaceURI(element)
: (element as any).namespaceURI;
};
// As per issue #47, the web-components registry is inherited by a
// new document created via createHTMLDocument. As per the spec
// (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries)
// a new empty registry is used when creating a template contents owner
// document, so we use that as our parent document to ensure nothing
// is inherited.
if (typeof HTMLTemplateElement === 'function') {
const template = document.createElement('template');
if (template.content && template.content.ownerDocument) {
document = template.content.ownerDocument;
}
}
let trustedTypesPolicy;
let emptyHTML = '';
// The instance's own internal Trusted Types policy. Unlike a caller-supplied
// `TRUSTED_TYPES_POLICY`, this is created at most once — Trusted Types throws
// on duplicate policy names — and is the only policy allowed to persist
// across configurations and survive `clearConfig()`.
let defaultTrustedTypesPolicy;
let defaultTrustedTypesPolicyResolved = false;
// Tracks whether we are already inside a call to the configured Trusted Types
// policy (`createHTML` or `createScriptURL`). If a supplied policy callback
// itself calls `DOMPurify.sanitize` (the cause of #1422), `sanitize` would
// re-enter the policy and recurse until the stack overflows. We detect that
// re-entry and throw a clear, actionable error instead. The guard is shared
// across both callbacks, because either one re-entering `sanitize` triggers
// the same unbounded recursion.
let IN_TRUSTED_TYPES_POLICY = 0;
const _assertNotInTrustedTypesPolicy = function (): void {
if (IN_TRUSTED_TYPES_POLICY > 0) {
throw typeErrorCreate(
'A configured TRUSTED_TYPES_POLICY callback (createHTML or ' +
'createScriptURL) must not call DOMPurify.sanitize, as that causes ' +
'infinite recursion. Do not pass a policy whose callbacks wrap ' +
'DOMPurify as TRUSTED_TYPES_POLICY; see the "DOMPurify and Trusted ' +
'Types" section of the README.'
);
}
};
const _createTrustedHTML = function (html: string): string {
_assertNotInTrustedTypesPolicy();
IN_TRUSTED_TYPES_POLICY++;
try {
return trustedTypesPolicy.createHTML(html);
} finally {
IN_TRUSTED_TYPES_POLICY--;
}
};
const _createTrustedScriptURL = function (scriptUrl: string): string {
_assertNotInTrustedTypesPolicy();
IN_TRUSTED_TYPES_POLICY++;
try {
return trustedTypesPolicy.createScriptURL(scriptUrl);
} finally {
IN_TRUSTED_TYPES_POLICY--;
}
};
// Lazily resolve (and cache) the instance's internal default policy.
// Resolution is attempted at most once: a successful `createPolicy` cannot be
// repeated (Trusted Types throws on duplicate names), and a failed or
// unsupported attempt must not be retried on every parse.
const _getDefaultTrustedTypesPolicy = function () {
if (!defaultTrustedTypesPolicyResolved) {
defaultTrustedTypesPolicy = _createTrustedTypesPolicy(
trustedTypes,
currentScript
);
defaultTrustedTypesPolicyResolved = true;
}
return defaultTrustedTypesPolicy;
};
const {
implementation,
createNodeIterator,
createDocumentFragment,
getElementsByTagName,
} = document;
const { importNode } = originalDocument;
let hooks = _createHooksMap();
/**
* Expose whether this browser supports running the full DOMPurify.
*/
DOMPurify.isSupported =
typeof entries === 'function' &&
typeof getParentNode === 'function' &&
implementation &&
implementation.createHTMLDocument !== undefined;
const {
MUSTACHE_EXPR,
ERB_EXPR,
TMPLIT_EXPR,
DATA_ATTR,
ARIA_ATTR,
IS_SCRIPT_OR_DATA,
ATTR_WHITESPACE,
CUSTOM_ELEMENT,
} = EXPRESSIONS;
let { IS_ALLOWED_URI } = EXPRESSIONS;
/**
* We consider the elements and attributes below to be safe. Ideally
* don't add any new ones but feel free to remove unwanted ones.
*/
/* allowed element names */
let ALLOWED_TAGS = null;
const DEFAULT_ALLOWED_TAGS = addToSet({}, [
...TAGS.html,
...TAGS.svg,
...TAGS.svgFilters,
...TAGS.mathMl,
...TAGS.text,
]);
/* Allowed attribute names */
let ALLOWED_ATTR = null;
const DEFAULT_ALLOWED_ATTR = addToSet({}, [
...ATTRS.html,
...ATTRS.svg,
...ATTRS.mathMl,
...ATTRS.xml,
]);
/*
* Configure how DOMPurify should handle custom elements and their attributes as well as customized built-in elements.
* @property {RegExp|Function|null} tagNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any custom elements)
* @property {RegExp|Function|null} attributeNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any attributes not on the allow list)
* @property {boolean} allowCustomizedBuiltInElements allow custom elements derived from built-ins if they pass CUSTOM_ELEMENT_HANDLING.tagNameCheck. Default: `false`.
*/
let CUSTOM_ELEMENT_HANDLING = Object.seal(
create(null, {
tagNameCheck: {
writable: true,
configurable: false,
enumerable: true,
value: null,
},
attributeNameCheck: {
writable: true,
configurable: false,
enumerable: true,
value: null,
},
allowCustomizedBuiltInElements: {
writable: true,
configurable: false,
enumerable: true,
value: false,
},
})
);
/* Explicitly forbidden tags (overrides ALLOWED_TAGS/ADD_TAGS) */
let FORBID_TAGS = null;
/* Explicitly forbidden attributes (overrides ALLOWED_ATTR/ADD_ATTR) */
let FORBID_ATTR = null;
/* Config object to store ADD_TAGS/ADD_ATTR functions (when used as functions) */
const EXTRA_ELEMENT_HANDLING = Object.seal(
create(null, {
tagCheck: {
writable: true,
configurable: false,
enumerable: true,
value: null,
},
attributeCheck: {
writable: true,
configurable: false,
enumerable: true,
value: null,
},
})
);
/* Decide if ARIA attributes are okay */
let ALLOW_ARIA_ATTR = true;
/* Decide if custom data attributes are okay */
let ALLOW_DATA_ATTR = true;
/* Decide if unknown protocols are okay */
let ALLOW_UNKNOWN_PROTOCOLS = false;
/* Decide if self-closing tags in attributes are allowed.
* Usually removed due to a mXSS issue in jQuery 3.0 */
let ALLOW_SELF_CLOSE_IN_ATTR = true;
/* Output should be safe for common template engines.
* This means, DOMPurify removes data attributes, mustaches and ERB
*/
let SAFE_FOR_TEMPLATES = false;
/* Output should be safe even for XML used within HTML and alike.
* This means, DOMPurify removes comments when containing risky content.
*/
let SAFE_FOR_XML = true;
/* Decide if document with ... should be returned */
let WHOLE_DOCUMENT = false;
/* Track whether config is already set on this instance of DOMPurify. */
let SET_CONFIG = false;
/* Pristine allowlist bindings captured at setConfig() time. On the
* persistent-config path sanitize() restores the sets from these before
* the per-walk hook clone-guard, so a hook's in-call widening cannot
* carry across calls. Null until setConfig() is called; reset by
* clearConfig(). */
let SET_CONFIG_ALLOWED_TAGS = null;
let SET_CONFIG_ALLOWED_ATTR = null;
/* Decide if all elements (e.g. style, script) must be children of
* document.body. By default, browsers might move them to document.head */
let FORCE_BODY = false;
/* Decide if a DOM `HTMLBodyElement` should be returned, instead of a html
* string (or a TrustedHTML object if Trusted Types are supported).
* If `WHOLE_DOCUMENT` is enabled a `HTMLHtmlElement` will be returned instead
*/
let RETURN_DOM = false;
/* Decide if a DOM `DocumentFragment` should be returned, instead of a html
* string (or a TrustedHTML object if Trusted Types are supported) */
let RETURN_DOM_FRAGMENT = false;
/* Try to return a Trusted Type object instead of a string, return a string in
* case Trusted Types are not supported */
let RETURN_TRUSTED_TYPE = false;
/* Output should be free from DOM clobbering attacks?
* This sanitizes markups named with colliding, clobberable built-in DOM APIs.
*/
let SANITIZE_DOM = true;
/* Achieve full DOM Clobbering protection by isolating the namespace of named
* properties and JS variables, mitigating attacks that abuse the HTML/DOM spec rules.
*
* HTML/DOM spec rules that enable DOM Clobbering:
* - Named Access on Window (§7.3.3)
* - DOM Tree Accessors (§3.1.5)
* - Form Element Parent-Child Relations (§4.10.3)
* - Iframe srcdoc / Nested WindowProxies (§4.8.5)
* - HTMLCollection (§4.2.10.2)
*
* Namespace isolation is implemented by prefixing `id` and `name` attributes
* with a constant string, i.e., `user-content-`
*/
let SANITIZE_NAMED_PROPS = false;
const SANITIZE_NAMED_PROPS_PREFIX = 'user-content-';
/* Keep element content when removing element? */
let KEEP_CONTENT = true;
/* If a `Node` is passed to sanitize(), then performs sanitization in-place instead
* of importing it into a new Document and returning a sanitized copy */
let IN_PLACE = false;
/* IN_PLACE: nodes processed during the current call */
let IN_PLACE_VISITED: Set | null = null;
/* Allow usage of profiles like html, svg and mathMl */
let USE_PROFILES: UseProfilesConfig | false = {};
/* Tags to ignore content of when KEEP_CONTENT is true */
let FORBID_CONTENTS = null;
const DEFAULT_FORBID_CONTENTS = addToSet({}, [
'annotation-xml',
'audio',
'colgroup',
'desc',
'foreignobject',
'head',
'iframe',
'math',
'mi',
'mn',
'mo',
'ms',
'mtext',
'noembed',
'noframes',
'noscript',
'plaintext',
'script',
// mirrors the selected