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, arraySplice, stringMatch, stringReplace, stringToLowerCase, stringToString, stringIndexOf, stringTrim, regExpTest, isRegex, typeErrorCreate, 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 }; 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; }; 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'); 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; // 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; /* 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