dompurify 3.4.10 → 3.4.12

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/src/purify.ts CHANGED
@@ -1,5 +1,3 @@
1
- /* eslint-disable @typescript-eslint/indent */
2
-
3
1
  import type { Config, UseProfilesConfig } from './config';
4
2
  import type { DOMPurify, HooksMap, HookFunction, WindowLike } from './types';
5
3
  import * as TAGS from './tags.js';
@@ -435,6 +433,14 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
435
433
  /* Track whether config is already set on this instance of DOMPurify. */
436
434
  let SET_CONFIG = false;
437
435
 
436
+ /* Pristine allowlist bindings captured at setConfig() time. On the
437
+ * persistent-config path sanitize() restores the sets from these before
438
+ * the per-walk hook clone-guard, so a hook's in-call widening cannot
439
+ * carry across calls. Null until setConfig() is called; reset by
440
+ * clearConfig(). */
441
+ let SET_CONFIG_ALLOWED_TAGS = null;
442
+ let SET_CONFIG_ALLOWED_ATTR = null;
443
+
438
444
  /* Decide if all elements (e.g. style, script) must be children of
439
445
  * document.body. By default, browsers might move them to document.head */
440
446
  let FORCE_BODY = false;
@@ -944,30 +950,6 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
944
950
  }
945
951
  }
946
952
 
947
- /*
948
- * Mirror the clone-before-mutate pattern already applied above for
949
- * cfg.ADD_TAGS / cfg.ADD_ATTR: if any uponSanitize* hook is
950
- * registered AND the set still points at the default constant,
951
- * clone it. The hook then mutates the clone (in-call widening
952
- * still works exactly as documented) and the next default-cfg
953
- * call rebinds to the untouched original via the reassignment at
954
- * the top of this function.
955
- */
956
- if (
957
- (hooks.uponSanitizeElement.length > 0 ||
958
- hooks.uponSanitizeAttribute.length > 0) &&
959
- ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS
960
- ) {
961
- ALLOWED_TAGS = clone(ALLOWED_TAGS);
962
- }
963
-
964
- if (
965
- hooks.uponSanitizeAttribute.length > 0 &&
966
- ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR
967
- ) {
968
- ALLOWED_ATTR = clone(ALLOWED_ATTR);
969
- }
970
-
971
953
  // Prevent further manipulation of configuration.
972
954
  // Not available in IE8, Safari 5, etc.
973
955
  if (freeze) {
@@ -1206,6 +1188,14 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
1206
1188
  * @param root the in-place root to empty
1207
1189
  */
1208
1190
  const _neutralizeRoot = function (root: Node): void {
1191
+ /* Strip every disallowed attribute (on* handlers included) off the whole
1192
+ subtree BEFORE detaching anything. Detaching first would hand back
1193
+ handler-bearing originals (e.g. an already-loading `<img onerror>`)
1194
+ whose queued resource event still fires in page scope after we throw.
1195
+ Clobber-safe reads; a doomed clobbered node's own attributes are
1196
+ irrelevant while its non-clobbered descendants are reached and scrubbed. */
1197
+ _neutralizeSubtree(root);
1198
+
1209
1199
  const childNodes = getChildNodes(root);
1210
1200
  if (childNodes) {
1211
1201
  const snapshot: Node[] = [];
@@ -1345,6 +1335,100 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
1345
1335
  }
1346
1336
  };
1347
1337
 
1338
+ /**
1339
+ * _neutralizePatchLinkage
1340
+ *
1341
+ * IN_PLACE entry pre-pass (declarative-partial-updates / streaming
1342
+ * hardening, https://github.com/WICG/declarative-partial-updates).
1343
+ *
1344
+ * The main walk strips patch linkage (`for`/`patchsrc`) and removes range
1345
+ * markers (PIs / markup comments) node-by-node, in document order, AS it
1346
+ * reaches each node. On a live in-place root that leaves a window: from the
1347
+ * moment the root is connected until the walk arrives at a given node, that
1348
+ * node's linkage is live. A patch applied on connection/stream can fire as
1349
+ * a microtask during the walk and inject or teleport an unsanitized DOM
1350
+ * range into a region the iterator has already passed and will not revisit,
1351
+ * so the post-return "tree is sanitized" contract is violated. Sweep the
1352
+ * whole tree once up front and sever every linkage before the walk begins,
1353
+ * closing that window.
1354
+ *
1355
+ * This CANNOT undo a patch that already fired before sanitize ran — that is
1356
+ * the irreducible "do not IN_PLACE a live-connected attacker tree" caveat —
1357
+ * but it closes everything from sanitize-start onward. Gated on SAFE_FOR_XML
1358
+ * to group with the rest of the declarative-partial-updates handling and
1359
+ * stay overridable, consistent with the codebase.
1360
+ *
1361
+ * Clobber-safe traversal (cached childNodes getter); per-node try/catch so a
1362
+ * clobbered root cannot defeat the sweep of its non-clobbered descendants.
1363
+ *
1364
+ * NOTE (pending real-Chrome confirmation, see test/declarative-patch-probe
1365
+ * .html Q1): this mirrors the existing policy of keeping `for` on
1366
+ * <label>/<output>. If the shipping feature can drive a patch through a
1367
+ * surviving `for`-on-label/output + `id` pair, this pre-pass and the
1368
+ * attribute check at _isBasicCustomElement's caller must additionally drop
1369
+ * that pair on the IN_PLACE path. Left as-is until the taxonomy is verified.
1370
+ *
1371
+ * @param root the in-place root to sweep
1372
+ */
1373
+ const _neutralizePatchLinkage = function (root: Node): void {
1374
+ if (!SAFE_FOR_XML) {
1375
+ return;
1376
+ }
1377
+
1378
+ const stack: Node[] = [root];
1379
+ while (stack.length > 0) {
1380
+ const node = stack.pop();
1381
+ const nodeType = getNodeType ? getNodeType(node) : (node as any).nodeType;
1382
+
1383
+ /* Remove range markers (the target side of a patch linkage): every
1384
+ processing instruction, and any markup-bearing comment. */
1385
+ if (
1386
+ nodeType === NODE_TYPE.processingInstruction ||
1387
+ (nodeType === NODE_TYPE.comment &&
1388
+ regExpTest(EXPRESSIONS.COMMENT_MARKUP_PROBE, (node as any).data))
1389
+ ) {
1390
+ try {
1391
+ remove(node);
1392
+ } catch (_) {
1393
+ /* Best-effort */
1394
+ }
1395
+
1396
+ continue;
1397
+ }
1398
+
1399
+ /* Strip patch-source attributes (the source side) off elements. */
1400
+ if (nodeType === NODE_TYPE.element) {
1401
+ const element = node as Element;
1402
+ const lcTag = transformCaseFunc(
1403
+ getNodeName ? getNodeName(node) : (node as any).nodeName
1404
+ );
1405
+ try {
1406
+ if (element.hasAttribute && element.hasAttribute('patchsrc')) {
1407
+ element.removeAttribute('patchsrc');
1408
+ }
1409
+
1410
+ if (
1411
+ element.hasAttribute &&
1412
+ element.hasAttribute('for') &&
1413
+ lcTag !== 'label' &&
1414
+ lcTag !== 'output'
1415
+ ) {
1416
+ element.removeAttribute('for');
1417
+ }
1418
+ } catch (_) {
1419
+ /* Clobbered removeAttribute/hasAttribute on a doomed node — ignore */
1420
+ }
1421
+ }
1422
+
1423
+ const childNodes = getChildNodes(node);
1424
+ if (childNodes) {
1425
+ for (let i = childNodes.length - 1; i >= 0; --i) {
1426
+ stack.push(childNodes[i]);
1427
+ }
1428
+ }
1429
+ }
1430
+ };
1431
+
1348
1432
  /**
1349
1433
  * _initDocument
1350
1434
  *
@@ -1679,9 +1763,15 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
1679
1763
  /**
1680
1764
  * Handle a node whose tag is forbidden or not allowlisted: keep
1681
1765
  * 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.
1766
+ * early - the namespace and fallback-tag removal checks are
1767
+ * intentionally skipped for kept custom elements), else hoist
1768
+ * content per KEEP_CONTENT and remove.
1769
+ *
1770
+ * A kept custom element is the ONLY case in which this function
1771
+ * returns false, so the caller uses that return value to run the
1772
+ * afterSanitizeElements hook on the kept element and keep the
1773
+ * element-hook lifecycle consistent with normal allowlisted
1774
+ * elements (GHSA-c2j3-45gr-mqc4).
1685
1775
  *
1686
1776
  * @param currentNode the disallowed node
1687
1777
  * @param tagName the node's transformCaseFunc'd tag name
@@ -1766,10 +1856,17 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
1766
1856
  * @param currentNode to check for permission to exist
1767
1857
  * @return true if node was killed, false if left alive
1768
1858
  */
1769
- const _sanitizeElements = function (currentNode: any): boolean {
1859
+ // eslint-disable-next-line complexity
1860
+ const _sanitizeElements = function (currentNode: any, root: Node): boolean {
1770
1861
  /* Execute a hook if present */
1771
1862
  _executeHooks(hooks.beforeSanitizeElements, currentNode, null);
1772
1863
 
1864
+ /* A hook may have detached the node — treat it as removed (see the
1865
+ detached-node comment after the uponSanitizeElement hook below). */
1866
+ if (currentNode !== root && getParentNode(currentNode) === null) {
1867
+ return true;
1868
+ }
1869
+
1773
1870
  /* Check if element is clobbered or can clobber */
1774
1871
  if (_isClobbered(currentNode)) {
1775
1872
  _forceRemove(currentNode);
@@ -1787,6 +1884,25 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
1787
1884
  allowedTags: ALLOWED_TAGS,
1788
1885
  });
1789
1886
 
1887
+ /* A hook may have detached the node from the tree — a long-standing
1888
+ user pattern (issue #469; draw.io-style foreignObject filtering).
1889
+ Per the cached, unclobberable parentNode getter the node is
1890
+ genuinely out of the tree, so it can reach neither the serialized
1891
+ output nor an IN_PLACE live tree; treat it as removed and stop
1892
+ processing it. Without this guard, the unsafe-node / namespace
1893
+ checks below would call _forceRemove on a parentless node and hit
1894
+ the REPORT-3 fail-closed throw — which exists for nodes DOMPurify
1895
+ wants gone but *cannot* detach (clobbered / parentless roots), the
1896
+ opposite of a node that is already safely gone. The walk root is
1897
+ exempt: a detached IN_PLACE root is legitimate input and must still
1898
+ be fully sanitized, and a kill-decision on it must keep hitting the
1899
+ REPORT-3 throw. Nodes detached by hooks are the hook's
1900
+ responsibility: they are not recorded in DOMPurify.removed and are
1901
+ not neutralized by the post-walk IN_PLACE pass. */
1902
+ if (currentNode !== root && getParentNode(currentNode) === null) {
1903
+ return true;
1904
+ }
1905
+
1790
1906
  /* Remove mXSS vectors, processing instructions and risky comments */
1791
1907
  if (_isUnsafeNode(currentNode, tagName)) {
1792
1908
  _forceRemove(currentNode);
@@ -1802,7 +1918,24 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
1802
1918
  ) &&
1803
1919
  !ALLOWED_TAGS[tagName])
1804
1920
  ) {
1805
- return _sanitizeDisallowedNode(currentNode, tagName);
1921
+ const removed = _sanitizeDisallowedNode(currentNode, tagName);
1922
+
1923
+ /* A false return means the node is a custom element kept via
1924
+ CUSTOM_ELEMENT_HANDLING - the only keep path through
1925
+ _sanitizeDisallowedNode. Run afterSanitizeElements on it so the
1926
+ element-hook lifecycle matches normal allowlisted elements: a
1927
+ security policy applied in this hook (e.g. stripping an attribute
1928
+ from every surviving element) must not silently skip kept custom
1929
+ elements (GHSA-c2j3-45gr-mqc4). This mirrors the normal-element
1930
+ tail below - the hook runs, then the walker's subsequent
1931
+ _sanitizeAttributes pass sanitizes the element's attributes. The
1932
+ deliberately skipped namespace and fallback-tag removal checks stay
1933
+ skipped; they are removal decisions, not the hook contract. */
1934
+ if (removed === false) {
1935
+ _executeHooks(hooks.afterSanitizeElements, currentNode, null);
1936
+ }
1937
+
1938
+ return removed;
1806
1939
  }
1807
1940
 
1808
1941
  /* Check whether element has a valid namespace.
@@ -1864,6 +1997,42 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
1864
1997
  return false;
1865
1998
  }
1866
1999
 
2000
+ /* Reject declarative-partial-updates patch-linkage attributes
2001
+ (https://github.com/WICG/declarative-partial-updates).
2002
+
2003
+ Empirical note (Chrome 150, verified — see
2004
+ test/declarative-patch-probe-v3.html): expansion is NOT applied after
2005
+ sanitization. For the string path it fires during sanitize()'s own
2006
+ parse, so the walk sees and sanitizes the fully materialized expanded
2007
+ tree — teleports into MathML/SVG integration points included; a
2008
+ weaponized `<template for>`->`<img onerror>` comes back with the handler
2009
+ stripped. For the IN_PLACE path it fires on connection, before the walk.
2010
+ Either way DOMPurify is NOT blind to the patch.
2011
+
2012
+ This removal is therefore defense-in-depth rather than the sole barrier:
2013
+ it prevents live linkage from surviving into the OUTPUT and re-expanding
2014
+ in the caller's context, and keeps behaviour deterministic if a future
2015
+ engine defers expansion. `for` is legitimate only on <label>/<output>;
2016
+ anywhere else (notably <template for>) it links the element to a patch
2017
+ target and teleports or removes an arbitrary DOM range by id/marker name.
2018
+ `patchsrc` fetches remote markup and is treated as a script-loading
2019
+ mechanism (CSP). Gated on SAFE_FOR_XML so the removal groups with the
2020
+ other structural-threat checks and stays overridable, consistent with
2021
+ the rest of the codebase. PI range markers are already removed by
2022
+ _isUnsafeNode. */
2023
+ if (SAFE_FOR_XML && lcName === 'patchsrc') {
2024
+ return false;
2025
+ }
2026
+
2027
+ if (
2028
+ SAFE_FOR_XML &&
2029
+ lcName === 'for' &&
2030
+ lcTag !== 'label' &&
2031
+ lcTag !== 'output'
2032
+ ) {
2033
+ return false;
2034
+ }
2035
+
1867
2036
  /* Make sure attribute cannot clobber */
1868
2037
  if (
1869
2038
  SANITIZE_DOM &&
@@ -2203,7 +2372,7 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
2203
2372
  _executeHooks(hooks.uponSanitizeShadowNode, shadowNode, null);
2204
2373
 
2205
2374
  /* Sanitize tags and elements */
2206
- _sanitizeElements(shadowNode);
2375
+ _sanitizeElements(shadowNode, fragment);
2207
2376
 
2208
2377
  /* Check attributes next */
2209
2378
  _sanitizeAttributes(shadowNode);
@@ -2366,10 +2535,37 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
2366
2535
  }
2367
2536
 
2368
2537
  /* Assign config vars */
2369
- if (!SET_CONFIG) {
2538
+ if (SET_CONFIG) {
2539
+ /* Persistent setConfig() path: _parseConfig is skipped, so the sets are
2540
+ * not re-derived per call. Restore them from the pristine bindings
2541
+ * captured at setConfig() time so a previous call's hook clone (mutated
2542
+ * below) does not carry over. */
2543
+ ALLOWED_TAGS = SET_CONFIG_ALLOWED_TAGS;
2544
+ ALLOWED_ATTR = SET_CONFIG_ALLOWED_ATTR;
2545
+ } else {
2370
2546
  _parseConfig(cfg);
2371
2547
  }
2372
2548
 
2549
+ /* Clone the hook-mutable allowlists before the walk whenever an
2550
+ * uponSanitize* hook is registered. The hook event exposes ALLOWED_TAGS
2551
+ * and ALLOWED_ATTR by reference (as allowedTags / allowedAttributes), so
2552
+ * a hook that widens them would otherwise mutate the shared set
2553
+ * permanently: across later calls and across every element. Cloning per
2554
+ * walk keeps documented in-call widening working while scoping it to the
2555
+ * call. A single guard for both config paths - the per-call path rebinds
2556
+ * the sets in _parseConfig each call, the persistent path restores them
2557
+ * from the captured bindings just above - so the two cannot diverge. */
2558
+ if (
2559
+ hooks.uponSanitizeElement.length > 0 ||
2560
+ hooks.uponSanitizeAttribute.length > 0
2561
+ ) {
2562
+ ALLOWED_TAGS = clone(ALLOWED_TAGS);
2563
+ }
2564
+
2565
+ if (hooks.uponSanitizeAttribute.length > 0) {
2566
+ ALLOWED_ATTR = clone(ALLOWED_ATTR);
2567
+ }
2568
+
2373
2569
  /* Clean up removed elements */
2374
2570
  DOMPurify.removed = [];
2375
2571
 
@@ -2382,6 +2578,12 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
2382
2578
  const inPlace = IN_PLACE && typeof dirty !== 'string' && _isNode(dirty);
2383
2579
 
2384
2580
  if (inPlace) {
2581
+ /* Declarative-partial-updates / streaming pre-pass: sever every patch
2582
+ linkage across the live tree BEFORE the walk, so no patch can fire
2583
+ mid-walk and inject into an already-processed region. Runs first, so
2584
+ it also covers the forbidden/clobbered roots that throw below. */
2585
+ _neutralizePatchLinkage(dirty as Node);
2586
+
2385
2587
  /* Do some early pre-sanitization to avoid unsafe root nodes.
2386
2588
  Read nodeName through the cached prototype getter — a clobbering
2387
2589
  child named "nodeName" on the form root would otherwise shadow
@@ -2393,6 +2595,10 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
2393
2595
  if (typeof nn === 'string') {
2394
2596
  const tagName = transformCaseFunc(nn);
2395
2597
  if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {
2598
+ /* Fail closed on a live root: neutralize handlers/children before
2599
+ throwing, exactly as the mid-walk abort path does. */
2600
+ _neutralizeRoot(dirty as Node);
2601
+
2396
2602
  throw typeErrorCreate(
2397
2603
  'root node is forbidden and cannot be sanitized in-place'
2398
2604
  );
@@ -2410,6 +2616,11 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
2410
2616
  the application unsanitized. Refuse to sanitize such a root
2411
2617
  the same way we refuse a forbidden tag. GHSA-r47g-fvhr-h676. */
2412
2618
  if (_isClobbered(dirty as Element)) {
2619
+ /* Fail closed on a live clobbered root before throwing.
2620
+ _neutralizeRoot's reads are clobber-safe (cached getters); the
2621
+ form's non-clobbered descendants, e.g. an armed <img>, are scrubbed. */
2622
+ _neutralizeRoot(dirty as Node);
2623
+
2413
2624
  throw typeErrorCreate(
2414
2625
  'root node is clobbered and cannot be sanitized in-place'
2415
2626
  );
@@ -2480,7 +2691,8 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
2480
2691
  }
2481
2692
 
2482
2693
  /* Get node iterator */
2483
- const nodeIterator = _createNodeIterator(inPlace ? dirty : body);
2694
+ const walkRoot: Node = inPlace ? (dirty as Node) : body;
2695
+ const nodeIterator = _createNodeIterator(walkRoot);
2484
2696
 
2485
2697
  /* Now start iterating over the created document.
2486
2698
  The walk runs inside an exception barrier (campaign-3 F2): a re-entrant
@@ -2494,7 +2706,7 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
2494
2706
  try {
2495
2707
  while ((currentNode = nodeIterator.nextNode())) {
2496
2708
  /* Sanitize tags and elements */
2497
- _sanitizeElements(currentNode);
2709
+ _sanitizeElements(currentNode, walkRoot);
2498
2710
 
2499
2711
  /* Check attributes next */
2500
2712
  _sanitizeAttributes(currentNode);
@@ -2510,6 +2722,14 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
2510
2722
  } catch (error) {
2511
2723
  if (inPlace) {
2512
2724
  _neutralizeRoot(dirty as Node);
2725
+ /* Nodes _forceRemove'd earlier in the aborted walk are already
2726
+ detached from the root, so _neutralizeRoot's subtree pass does not
2727
+ reach them. Defuse them too, mirroring the success-path loop below. */
2728
+ arrayForEach(DOMPurify.removed, (entry) => {
2729
+ if (entry.element) {
2730
+ _neutralizeSubtree(entry.element as Node);
2731
+ }
2732
+ });
2513
2733
  }
2514
2734
 
2515
2735
  throw error;
@@ -2596,11 +2816,15 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
2596
2816
  DOMPurify.setConfig = function (cfg = {}) {
2597
2817
  _parseConfig(cfg);
2598
2818
  SET_CONFIG = true;
2819
+ SET_CONFIG_ALLOWED_TAGS = ALLOWED_TAGS;
2820
+ SET_CONFIG_ALLOWED_ATTR = ALLOWED_ATTR;
2599
2821
  };
2600
2822
 
2601
2823
  DOMPurify.clearConfig = function () {
2602
2824
  CONFIG = null;
2603
2825
  SET_CONFIG = false;
2826
+ SET_CONFIG_ALLOWED_TAGS = null;
2827
+ SET_CONFIG_ALLOWED_ATTR = null;
2604
2828
 
2605
2829
  // Drop any caller-supplied Trusted Types policy so it cannot poison later
2606
2830
  // `RETURN_TRUSTED_TYPE` output. The internal default policy (cached, and
@@ -2629,6 +2853,14 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
2629
2853
  return;
2630
2854
  }
2631
2855
 
2856
+ /* Reject unknown entry points. Without this, a non-hook key (e.g.
2857
+ * '__proto__') indexes off the prototype chain rather than a real
2858
+ * hook array, and arrayPush then writes to Object.prototype. Guard
2859
+ * with an own-property check against the known hook names. */
2860
+ if (!objectHasOwnProperty(hooks, entryPoint)) {
2861
+ return;
2862
+ }
2863
+
2632
2864
  arrayPush(hooks[entryPoint], hookFunction);
2633
2865
  };
2634
2866
 
@@ -2636,6 +2868,10 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
2636
2868
  entryPoint: keyof HooksMap,
2637
2869
  hookFunction: HookFunction
2638
2870
  ) {
2871
+ if (!objectHasOwnProperty(hooks, entryPoint)) {
2872
+ return undefined;
2873
+ }
2874
+
2639
2875
  if (hookFunction !== undefined) {
2640
2876
  const index = arrayLastIndexOf(hooks[entryPoint], hookFunction);
2641
2877
 
@@ -2648,6 +2884,10 @@ function createDOMPurify(window: WindowLike = getGlobal()): DOMPurify {
2648
2884
  };
2649
2885
 
2650
2886
  DOMPurify.removeHooks = function (entryPoint: keyof HooksMap) {
2887
+ if (!objectHasOwnProperty(hooks, entryPoint)) {
2888
+ return;
2889
+ }
2890
+
2651
2891
  hooks[entryPoint] = [];
2652
2892
  };
2653
2893
 
package/src/types.ts CHANGED
@@ -1,5 +1,3 @@
1
- /* eslint-disable @typescript-eslint/indent */
2
-
3
1
  import type {
4
2
  TrustedHTML,
5
3
  TrustedTypesWindow,