Skip to content

Commit 6ea80cd

Browse files
authored
Merge pull request #943 from cure53/main
Merging fixes covering nesting-based mXSS into 3.x branch
2 parents db19269 + c0d418c commit 6ea80cd

12 files changed

+326
-29
lines changed

README.md

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ After sanitizing your markup, you can also have a look at the property `DOMPurif
7373

7474
DOMPurify technically also works server-side with Node.js. Our support strives to follow the [Node.js release cycle](https://nodejs.org/en/about/releases/).
7575

76-
Running DOMPurify on the server requires a DOM to be present, which is probably no surprise. Usually, [jsdom](https://github.com/jsdom/jsdom) is the tool of choice and we **strongly recommend** to use the latest version of _jsdom_.
76+
Running DOMPurify on the server requires a DOM to be present, which is probably no surprise. Usually, [jsdom](https://github.com/jsdom/jsdom) is the tool of choice and we **strongly recommend** to use the latest version of _jsdom_.
7777

7878
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.
7979

@@ -158,6 +158,15 @@ In version 2.0.0, a config flag was added to control DOMPurify's behavior regard
158158

159159
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).
160160

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+
161170
## Can I configure DOMPurify?
162171

163172
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).
@@ -360,11 +369,11 @@ _Example_:
360369
361370
```js
362371
DOMPurify.addHook(
363-
'beforeSanitizeElements',
372+
'uponSanitizeAttribute',
364373
function (currentNode, hookEvent, config) {
365-
// Do something with the current node and return it
366-
// You can also mutate hookEvent (i.e. set hookEvent.forceKeepAttr = true)
367-
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
368377
}
369378
);
370379
```

dist/purify.cjs.js

Lines changed: 49 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/purify.cjs.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/purify.es.mjs

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,9 @@ function createDOMPurify() {
515515
/* Keep a reference to config to pass to hooks */
516516
let CONFIG = null;
517517

518+
/* Specify the maximum element nesting depth to prevent mXSS */
519+
const MAX_NESTING_DEPTH = 500;
520+
518521
/* Ideally, do not touch anything below this line */
519522
/* ______________________________________________ */
520523

@@ -925,7 +928,11 @@ function createDOMPurify() {
925928
* @return {Boolean} true if clobbered, false if safe
926929
*/
927930
const _isClobbered = function _isClobbered(elm) {
928-
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');
931+
return elm instanceof HTMLFormElement && (
932+
// eslint-disable-next-line unicorn/no-typeof-undefined
933+
typeof elm.__depth !== 'undefined' && typeof elm.__depth !== 'number' ||
934+
// eslint-disable-next-line unicorn/no-typeof-undefined
935+
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');
929936
};
930937

931938
/**
@@ -1023,7 +1030,9 @@ function createDOMPurify() {
10231030
if (childNodes && parentNode) {
10241031
const childCount = childNodes.length;
10251032
for (let i = childCount - 1; i >= 0; --i) {
1026-
parentNode.insertBefore(cloneNode(childNodes[i], true), getNextSibling(currentNode));
1033+
const childClone = cloneNode(childNodes[i], true);
1034+
childClone.__removalCount = (currentNode.__removalCount || 0) + 1;
1035+
parentNode.insertBefore(childClone, getNextSibling(currentNode));
10271036
}
10281037
}
10291038
}
@@ -1256,8 +1265,27 @@ function createDOMPurify() {
12561265
continue;
12571266
}
12581267

1268+
/* Set the nesting depth of an element */
1269+
if (shadowNode.nodeType === 1) {
1270+
if (shadowNode.parentNode && shadowNode.parentNode.__depth) {
1271+
/*
1272+
We want the depth of the node in the original tree, which can
1273+
change when it's removed from its parent.
1274+
*/
1275+
shadowNode.__depth = (shadowNode.__removalCount || 0) + shadowNode.parentNode.__depth + 1;
1276+
} else {
1277+
shadowNode.__depth = 1;
1278+
}
1279+
}
1280+
1281+
/* Remove an element if nested too deeply to avoid mXSS */
1282+
if (shadowNode.__depth >= MAX_NESTING_DEPTH) {
1283+
_forceRemove(shadowNode);
1284+
}
1285+
12591286
/* Deep shadow DOM detected */
12601287
if (shadowNode.content instanceof DocumentFragment) {
1288+
shadowNode.content.__depth = shadowNode.__depth;
12611289
_sanitizeShadowDOM(shadowNode.content);
12621290
}
12631291

@@ -1374,8 +1402,27 @@ function createDOMPurify() {
13741402
continue;
13751403
}
13761404

1405+
/* Set the nesting depth of an element */
1406+
if (currentNode.nodeType === 1) {
1407+
if (currentNode.parentNode && currentNode.parentNode.__depth) {
1408+
/*
1409+
We want the depth of the node in the original tree, which can
1410+
change when it's removed from its parent.
1411+
*/
1412+
currentNode.__depth = (currentNode.__removalCount || 0) + currentNode.parentNode.__depth + 1;
1413+
} else {
1414+
currentNode.__depth = 1;
1415+
}
1416+
}
1417+
1418+
/* Remove an element if nested too deeply to avoid mXSS */
1419+
if (currentNode.__depth >= MAX_NESTING_DEPTH) {
1420+
_forceRemove(currentNode);
1421+
}
1422+
13771423
/* Shadow DOM detected, sanitize it */
13781424
if (currentNode.content instanceof DocumentFragment) {
1425+
currentNode.content.__depth = currentNode.__depth;
13791426
_sanitizeShadowDOM(currentNode.content);
13801427
}
13811428

dist/purify.es.mjs.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/purify.js

Lines changed: 49 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/purify.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/purify.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/purify.min.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)