dompurify 3.0.11 → 3.1.1

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/LICENSE CHANGED
@@ -1,5 +1,5 @@
1
1
  DOMPurify
2
- Copyright 2023 Dr.-Ing. Mario Heiderich, Cure53
2
+ Copyright 2024 Dr.-Ing. Mario Heiderich, Cure53
3
3
 
4
4
  DOMPurify is free software; you can redistribute it and/or modify it under the
5
5
  terms of either:
package/README.md CHANGED
@@ -6,11 +6,11 @@
6
6
 
7
7
  DOMPurify is a DOM-only, super-fast, uber-tolerant XSS sanitizer for HTML, MathML and SVG.
8
8
 
9
- It's also very simple to use and get started with. DOMPurify was [started in February 2014](https://github.com/cure53/DOMPurify/commit/a630922616927373485e0e787ab19e73e3691b2b) and, meanwhile, has reached version **v3.0.11**.
9
+ It's also very simple to use and get started with. DOMPurify was [started in February 2014](https://github.com/cure53/DOMPurify/commit/a630922616927373485e0e787ab19e73e3691b2b) and, meanwhile, has reached version **v3.1.1**.
10
10
 
11
11
  DOMPurify is written in JavaScript and works in all modern browsers (Safari (10+), Opera (15+), Edge, Firefox and Chrome - as well as almost anything else using Blink, Gecko or WebKit). It doesn't break on MSIE or other legacy browsers. It simply does nothing.
12
12
 
13
- **Note that [DOMPurify v2.4.9](https://github.com/cure53/DOMPurify/releases/tag/2.4.9) is the latest version supporting MSIE. For important security updates compatible with MSIE, please use the [2.x branch](https://github.com/cure53/DOMPurify/tree/2.x).**
13
+ **Note that [DOMPurify v2.5.1](https://github.com/cure53/DOMPurify/releases/tag/2.5.1) is the latest version supporting MSIE. For important security updates compatible with MSIE, please use the [2.x branch](https://github.com/cure53/DOMPurify/tree/2.x).**
14
14
 
15
15
  Our automated tests cover [19 different browsers](https://github.com/cure53/DOMPurify/blob/main/test/karma.custom-launchers.config.js#L5) right now, more to come. We also cover Node.js v16.x, v17.x, v18.x and v19.x, running DOMPurify on [jsdom](https://github.com/jsdom/jsdom). Older Node versions are known to work as well, but hey... no guarantees.
16
16
 
@@ -77,6 +77,8 @@ Running DOMPurify on the server requires a DOM to be present, which is probably
77
77
 
78
78
  Why? Because older versions of _jsdom_ are known to be buggy in ways that result in XSS _even if_ DOMPurify does everything 100% correctly. There are **known attack vectors** in, e.g. _jsdom v19.0.0_ that are fixed in _jsdom v20.0.0_ - and we really recommend to keep _jsdom_ up to date because of that.
79
79
 
80
+ Please also be aware that tools like [happy-dom](https://github.com/capricorn86/happy-dom) exist but **are not considered safe** at this point. Combining DOMPurify with _happy-dom_ is currently not recommended and will likely lead to XSS.
81
+
80
82
  Other than that, you are fine to use DOMPurify on the server. Probably. This really depends on _jsdom_ or whatever DOM you utilize server-side. If you can live with that, this is how you get it to work:
81
83
 
82
84
  ```bash
@@ -156,6 +158,15 @@ In version 2.0.0, a config flag was added to control DOMPurify's behavior regard
156
158
 
157
159
  When `DOMPurify.sanitize` is used in an environment where the Trusted Types API is available and `RETURN_TRUSTED_TYPE` is set to `true`, it tries to return a `TrustedHTML` value instead of a string (the behavior for `RETURN_DOM` and `RETURN_DOM_FRAGMENT` config options does not change).
158
160
 
161
+ Note that in order to create a policy in `trustedTypes` using DOMPurify, `RETURN_TRUSTED_TYPE: false` is required, as `createHTML` expects a normal string, not `TrustedHTML`. The example below shows this.
162
+
163
+ ```js
164
+ window.trustedTypes!.createPolicy('default', {
165
+ createHTML: (to_escape) =>
166
+ DOMPurify.sanitize(to_escape, { RETURN_TRUSTED_TYPE: false }),
167
+ });
168
+ ```
169
+
159
170
  ## Can I configure DOMPurify?
160
171
 
161
172
  Yes. The included default configuration values are pretty good already - but you can of course override them. Check out the [`/demos`](https://github.com/cure53/DOMPurify/tree/main/demos) folder to see a bunch of examples on how you can [customize DOMPurify](https://github.com/cure53/DOMPurify/tree/main/demos#what-is-this).
@@ -167,6 +178,10 @@ Yes. The included default configuration values are pretty good already - but you
167
178
  // allowing template parsing in user-controlled HTML is not advised at all.
168
179
  // only use this mode if there is really no alternative.
169
180
  const clean = DOMPurify.sanitize(dirty, {SAFE_FOR_TEMPLATES: true});
181
+
182
+
183
+ // change how e.g. comments containing risky HTML characters are treated.
184
+ const clean = DOMPurify.sanitize(dirty, {SAFE_FOR_XML: false});
170
185
  ```
171
186
 
172
187
  ### Control our allow-lists and block-lists
@@ -354,11 +369,11 @@ _Example_:
354
369
 
355
370
  ```js
356
371
  DOMPurify.addHook(
357
- 'beforeSanitizeElements',
372
+ 'uponSanitizeAttribute',
358
373
  function (currentNode, hookEvent, config) {
359
- // Do something with the current node and return it
360
- // You can also mutate hookEvent (i.e. set hookEvent.forceKeepAttr = true)
361
- return currentNode;
374
+ // Do something with the current node
375
+ // You can also mutate hookEvent for current node (i.e. set hookEvent.forceKeepAttr = true)
376
+ // For other than 'uponSanitizeAttribute' hook types hookEvent equals to null
362
377
  }
363
378
  );
364
379
  ```
@@ -1,4 +1,4 @@
1
- /*! @license DOMPurify 3.0.11 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.0.11/LICENSE */
1
+ /*! @license DOMPurify 3.1.1 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.1.1/LICENSE */
2
2
 
3
3
  'use strict';
4
4
 
@@ -198,7 +198,7 @@ const mathMl$1 = freeze(['math', 'menclose', 'merror', 'mfenced', 'mfrac', 'mgly
198
198
  const mathMlDisallowed = freeze(['maction', 'maligngroup', 'malignmark', 'mlongdiv', 'mscarries', 'mscarry', 'msgroup', 'mstack', 'msline', 'msrow', 'semantics', 'annotation', 'annotation-xml', 'mprescripts', 'none']);
199
199
  const text = freeze(['#text']);
200
200
 
201
- const html = freeze(['accept', 'action', 'align', 'alt', 'autocapitalize', 'autocomplete', 'autopictureinpicture', 'autoplay', 'background', 'bgcolor', 'border', 'capture', 'cellpadding', 'cellspacing', 'checked', 'cite', 'class', 'clear', 'color', 'cols', 'colspan', 'controls', 'controlslist', 'coords', 'crossorigin', 'datetime', 'decoding', 'default', 'dir', 'disabled', 'disablepictureinpicture', 'disableremoteplayback', 'download', 'draggable', 'enctype', 'enterkeyhint', 'face', 'for', 'headers', 'height', 'hidden', 'high', 'href', 'hreflang', 'id', 'inputmode', 'integrity', 'ismap', 'kind', 'label', 'lang', 'list', 'loading', 'loop', 'low', 'max', 'maxlength', 'media', 'method', 'min', 'minlength', 'multiple', 'muted', 'name', 'nonce', 'noshade', 'novalidate', 'nowrap', 'open', 'optimum', 'pattern', 'placeholder', 'playsinline', 'poster', 'preload', 'pubdate', 'radiogroup', 'readonly', 'rel', 'required', 'rev', 'reversed', 'role', 'rows', 'rowspan', 'spellcheck', 'scope', 'selected', 'shape', 'size', 'sizes', 'span', 'srclang', 'start', 'src', 'srcset', 'step', 'style', 'summary', 'tabindex', 'title', 'translate', 'type', 'usemap', 'valign', 'value', 'width', 'xmlns', 'slot']);
201
+ const html = freeze(['accept', 'action', 'align', 'alt', 'autocapitalize', 'autocomplete', 'autopictureinpicture', 'autoplay', 'background', 'bgcolor', 'border', 'capture', 'cellpadding', 'cellspacing', 'checked', 'cite', 'class', 'clear', 'color', 'cols', 'colspan', 'controls', 'controlslist', 'coords', 'crossorigin', 'datetime', 'decoding', 'default', 'dir', 'disabled', 'disablepictureinpicture', 'disableremoteplayback', 'download', 'draggable', 'enctype', 'enterkeyhint', 'face', 'for', 'headers', 'height', 'hidden', 'high', 'href', 'hreflang', 'id', 'inputmode', 'integrity', 'ismap', 'kind', 'label', 'lang', 'list', 'loading', 'loop', 'low', 'max', 'maxlength', 'media', 'method', 'min', 'minlength', 'multiple', 'muted', 'name', 'nonce', 'noshade', 'novalidate', 'nowrap', 'open', 'optimum', 'pattern', 'placeholder', 'playsinline', 'poster', 'preload', 'pubdate', 'radiogroup', 'readonly', 'rel', 'required', 'rev', 'reversed', 'role', 'rows', 'rowspan', 'spellcheck', 'scope', 'selected', 'shape', 'size', 'sizes', 'span', 'srclang', 'start', 'src', 'srcset', 'step', 'style', 'summary', 'tabindex', 'title', 'translate', 'type', 'usemap', 'valign', 'value', 'width', 'wrap', 'xmlns', 'slot']);
202
202
  const svg = freeze(['accent-height', 'accumulate', 'additive', 'alignment-baseline', 'ascent', 'attributename', 'attributetype', 'azimuth', 'basefrequency', 'baseline-shift', 'begin', 'bias', 'by', 'class', 'clip', 'clippathunits', 'clip-path', 'clip-rule', 'color', 'color-interpolation', 'color-interpolation-filters', 'color-profile', 'color-rendering', 'cx', 'cy', 'd', 'dx', 'dy', 'diffuseconstant', 'direction', 'display', 'divisor', 'dur', 'edgemode', 'elevation', 'end', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'filterunits', 'flood-color', 'flood-opacity', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'fx', 'fy', 'g1', 'g2', 'glyph-name', 'glyphref', 'gradientunits', 'gradienttransform', 'height', 'href', 'id', 'image-rendering', 'in', 'in2', 'k', 'k1', 'k2', 'k3', 'k4', 'kerning', 'keypoints', 'keysplines', 'keytimes', 'lang', 'lengthadjust', 'letter-spacing', 'kernelmatrix', 'kernelunitlength', 'lighting-color', 'local', 'marker-end', 'marker-mid', 'marker-start', 'markerheight', 'markerunits', 'markerwidth', 'maskcontentunits', 'maskunits', 'max', 'mask', 'media', 'method', 'mode', 'min', 'name', 'numoctaves', 'offset', 'operator', 'opacity', 'order', 'orient', 'orientation', 'origin', 'overflow', 'paint-order', 'path', 'pathlength', 'patterncontentunits', 'patterntransform', 'patternunits', 'points', 'preservealpha', 'preserveaspectratio', 'primitiveunits', 'r', 'rx', 'ry', 'radius', 'refx', 'refy', 'repeatcount', 'repeatdur', 'restart', 'result', 'rotate', 'scale', 'seed', 'shape-rendering', 'specularconstant', 'specularexponent', 'spreadmethod', 'startoffset', 'stddeviation', 'stitchtiles', 'stop-color', 'stop-opacity', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke', 'stroke-width', 'style', 'surfacescale', 'systemlanguage', 'tabindex', 'targetx', 'targety', 'transform', 'transform-origin', 'text-anchor', 'text-decoration', 'text-rendering', 'textlength', 'type', 'u1', 'u2', 'unicode', 'values', 'viewbox', 'visibility', 'version', 'vert-adv-y', 'vert-origin-x', 'vert-origin-y', 'width', 'word-spacing', 'wrap', 'writing-mode', 'xchannelselector', 'ychannelselector', 'x', 'x1', 'x2', 'xmlns', 'y', 'y1', 'y2', 'z', 'zoomandpan']);
203
203
  const mathMl = freeze(['accent', 'accentunder', 'align', 'bevelled', 'close', 'columnsalign', 'columnlines', 'columnspan', 'denomalign', 'depth', 'dir', 'display', 'displaystyle', 'encoding', 'fence', 'frame', 'height', 'href', 'id', 'largeop', 'length', 'linethickness', 'lspace', 'lquote', 'mathbackground', 'mathcolor', 'mathsize', 'mathvariant', 'maxsize', 'minsize', 'movablelimits', 'notation', 'numalign', 'open', 'rowalign', 'rowlines', 'rowspacing', 'rowspan', 'rspace', 'rquote', 'scriptlevel', 'scriptminsize', 'scriptsizemultiplier', 'selection', 'separator', 'separators', 'stretchy', 'subscriptshift', 'supscriptshift', 'symmetric', 'voffset', 'width', 'xmlns']);
204
204
  const xml = freeze(['xlink:href', 'xml:id', 'xlink:title', 'xml:space', 'xmlns:xlink']);
@@ -284,7 +284,7 @@ function createDOMPurify() {
284
284
  * Version label, exposed for easier checks
285
285
  * if DOMPurify is up to date or not
286
286
  */
287
- DOMPurify.version = '3.0.11';
287
+ DOMPurify.version = '3.1.1';
288
288
 
289
289
  /**
290
290
  * Array of elements that DOMPurify removed during sanitation.
@@ -426,6 +426,11 @@ function createDOMPurify() {
426
426
  */
427
427
  let SAFE_FOR_TEMPLATES = false;
428
428
 
429
+ /* Output should be safe even for XML used within HTML and alike.
430
+ * This means, DOMPurify removes comments when containing risky content.
431
+ */
432
+ let SAFE_FOR_XML = true;
433
+
429
434
  /* Decide if document with <html>... should be returned */
430
435
  let WHOLE_DOCUMENT = false;
431
436
 
@@ -512,6 +517,9 @@ function createDOMPurify() {
512
517
  /* Keep a reference to config to pass to hooks */
513
518
  let CONFIG = null;
514
519
 
520
+ /* Specify the maximum element nesting depth to prevent mXSS */
521
+ const MAX_NESTING_DEPTH = 255;
522
+
515
523
  /* Ideally, do not touch anything below this line */
516
524
  /* ______________________________________________ */
517
525
 
@@ -573,6 +581,7 @@ function createDOMPurify() {
573
581
  ALLOW_UNKNOWN_PROTOCOLS = cfg.ALLOW_UNKNOWN_PROTOCOLS || false; // Default false
574
582
  ALLOW_SELF_CLOSE_IN_ATTR = cfg.ALLOW_SELF_CLOSE_IN_ATTR !== false; // Default true
575
583
  SAFE_FOR_TEMPLATES = cfg.SAFE_FOR_TEMPLATES || false; // Default false
584
+ SAFE_FOR_XML = cfg.SAFE_FOR_XML !== false; // Default true
576
585
  WHOLE_DOCUMENT = cfg.WHOLE_DOCUMENT || false; // Default false
577
586
  RETURN_DOM = cfg.RETURN_DOM || false; // Default false
578
587
  RETURN_DOM_FRAGMENT = cfg.RETURN_DOM_FRAGMENT || false; // Default false
@@ -921,7 +930,11 @@ function createDOMPurify() {
921
930
  * @return {Boolean} true if clobbered, false if safe
922
931
  */
923
932
  const _isClobbered = function _isClobbered(elm) {
924
- return elm instanceof HTMLFormElement && (typeof elm.nodeName !== 'string' || typeof elm.textContent !== 'string' || typeof elm.removeChild !== 'function' || !(elm.attributes instanceof NamedNodeMap) || typeof elm.removeAttribute !== 'function' || typeof elm.setAttribute !== 'function' || typeof elm.namespaceURI !== 'string' || typeof elm.insertBefore !== 'function' || typeof elm.hasChildNodes !== 'function');
933
+ return elm instanceof HTMLFormElement && (
934
+ // eslint-disable-next-line unicorn/no-typeof-undefined
935
+ typeof elm.__depth !== 'undefined' && typeof elm.__depth !== 'number' ||
936
+ // eslint-disable-next-line unicorn/no-typeof-undefined
937
+ typeof elm.__removalCount !== 'undefined' && typeof elm.__removalCount !== 'number' || typeof elm.nodeName !== 'string' || typeof elm.textContent !== 'string' || typeof elm.removeChild !== 'function' || !(elm.attributes instanceof NamedNodeMap) || typeof elm.removeAttribute !== 'function' || typeof elm.setAttribute !== 'function' || typeof elm.namespaceURI !== 'string' || typeof elm.insertBefore !== 'function' || typeof elm.hasChildNodes !== 'function');
925
938
  };
926
939
 
927
940
  /**
@@ -994,6 +1007,12 @@ function createDOMPurify() {
994
1007
  return true;
995
1008
  }
996
1009
 
1010
+ /* Remove any kind of possibly harmful comments */
1011
+ if (SAFE_FOR_XML && currentNode.nodeType === 8 && regExpTest(/<[/\w]/g, currentNode.data)) {
1012
+ _forceRemove(currentNode);
1013
+ return true;
1014
+ }
1015
+
997
1016
  /* Remove element if anything forbids its presence */
998
1017
  if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {
999
1018
  /* Check if we have a custom element to handle */
@@ -1013,7 +1032,9 @@ function createDOMPurify() {
1013
1032
  if (childNodes && parentNode) {
1014
1033
  const childCount = childNodes.length;
1015
1034
  for (let i = childCount - 1; i >= 0; --i) {
1016
- parentNode.insertBefore(cloneNode(childNodes[i], true), getNextSibling(currentNode));
1035
+ const childClone = cloneNode(childNodes[i], true);
1036
+ childClone.__removalCount = (currentNode.__removalCount || 0) + 1;
1037
+ parentNode.insertBefore(childClone, getNextSibling(currentNode));
1017
1038
  }
1018
1039
  }
1019
1040
  }
@@ -1246,8 +1267,27 @@ function createDOMPurify() {
1246
1267
  continue;
1247
1268
  }
1248
1269
 
1270
+ /* Set the nesting depth of an element */
1271
+ if (shadowNode.nodeType === 1) {
1272
+ if (shadowNode.parentNode && shadowNode.parentNode.__depth) {
1273
+ /*
1274
+ We want the depth of the node in the original tree, which can
1275
+ change when it's removed from its parent.
1276
+ */
1277
+ shadowNode.__depth = (shadowNode.__removalCount || 0) + shadowNode.parentNode.__depth + 1;
1278
+ } else {
1279
+ shadowNode.__depth = 1;
1280
+ }
1281
+ }
1282
+
1283
+ /* Remove an element if nested too deeply to avoid mXSS */
1284
+ if (shadowNode.__depth >= MAX_NESTING_DEPTH) {
1285
+ _forceRemove(shadowNode);
1286
+ }
1287
+
1249
1288
  /* Deep shadow DOM detected */
1250
1289
  if (shadowNode.content instanceof DocumentFragment) {
1290
+ shadowNode.content.__depth = shadowNode.__depth;
1251
1291
  _sanitizeShadowDOM(shadowNode.content);
1252
1292
  }
1253
1293
 
@@ -1364,8 +1404,27 @@ function createDOMPurify() {
1364
1404
  continue;
1365
1405
  }
1366
1406
 
1407
+ /* Set the nesting depth of an element */
1408
+ if (currentNode.nodeType === 1) {
1409
+ if (currentNode.parentNode && currentNode.parentNode.__depth) {
1410
+ /*
1411
+ We want the depth of the node in the original tree, which can
1412
+ change when it's removed from its parent.
1413
+ */
1414
+ currentNode.__depth = (currentNode.__removalCount || 0) + currentNode.parentNode.__depth + 1;
1415
+ } else {
1416
+ currentNode.__depth = 1;
1417
+ }
1418
+ }
1419
+
1420
+ /* Remove an element if nested too deeply to avoid mXSS */
1421
+ if (currentNode.__depth >= MAX_NESTING_DEPTH) {
1422
+ _forceRemove(currentNode);
1423
+ }
1424
+
1367
1425
  /* Shadow DOM detected, sanitize it */
1368
1426
  if (currentNode.content instanceof DocumentFragment) {
1427
+ currentNode.content.__depth = currentNode.__depth;
1369
1428
  _sanitizeShadowDOM(currentNode.content);
1370
1429
  }
1371
1430