Skip to content

Commit

Permalink
Merge pull request #943 from cure53/main
Browse files Browse the repository at this point in the history
Merging fixes covering nesting-based mXSS into 3.x branch
  • Loading branch information
cure53 committed Apr 25, 2024
2 parents db19269 + c0d418c commit 6ea80cd
Show file tree
Hide file tree
Showing 12 changed files with 326 additions and 29 deletions.
19 changes: 14 additions & 5 deletions README.md
Expand Up @@ -73,7 +73,7 @@ After sanitizing your markup, you can also have a look at the property `DOMPurif

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/).

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_.
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_.

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.

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

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

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.

```js
window.trustedTypes!.createPolicy('default', {
createHTML: (to_escape) =>
DOMPurify.sanitize(to_escape, { RETURN_TRUSTED_TYPE: false }),
});
```

## Can I configure DOMPurify?

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).
Expand Down Expand Up @@ -360,11 +369,11 @@ _Example_:
```js
DOMPurify.addHook(
'beforeSanitizeElements',
'uponSanitizeAttribute',
function (currentNode, hookEvent, config) {
// Do something with the current node and return it
// You can also mutate hookEvent (i.e. set hookEvent.forceKeepAttr = true)
return currentNode;
// Do something with the current node
// You can also mutate hookEvent for current node (i.e. set hookEvent.forceKeepAttr = true)
// For other than 'uponSanitizeAttribute' hook types hookEvent equals to null
}
);
```
Expand Down
51 changes: 49 additions & 2 deletions dist/purify.cjs.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dist/purify.cjs.js.map

Large diffs are not rendered by default.

51 changes: 49 additions & 2 deletions dist/purify.es.mjs
Expand Up @@ -515,6 +515,9 @@ function createDOMPurify() {
/* Keep a reference to config to pass to hooks */
let CONFIG = null;

/* Specify the maximum element nesting depth to prevent mXSS */
const MAX_NESTING_DEPTH = 500;

/* Ideally, do not touch anything below this line */
/* ______________________________________________ */

Expand Down Expand Up @@ -925,7 +928,11 @@ function createDOMPurify() {
* @return {Boolean} true if clobbered, false if safe
*/
const _isClobbered = function _isClobbered(elm) {
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');
return elm instanceof HTMLFormElement && (
// eslint-disable-next-line unicorn/no-typeof-undefined
typeof elm.__depth !== 'undefined' && typeof elm.__depth !== 'number' ||
// eslint-disable-next-line unicorn/no-typeof-undefined
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');
};

/**
Expand Down Expand Up @@ -1023,7 +1030,9 @@ function createDOMPurify() {
if (childNodes && parentNode) {
const childCount = childNodes.length;
for (let i = childCount - 1; i >= 0; --i) {
parentNode.insertBefore(cloneNode(childNodes[i], true), getNextSibling(currentNode));
const childClone = cloneNode(childNodes[i], true);
childClone.__removalCount = (currentNode.__removalCount || 0) + 1;
parentNode.insertBefore(childClone, getNextSibling(currentNode));
}
}
}
Expand Down Expand Up @@ -1256,8 +1265,27 @@ function createDOMPurify() {
continue;
}

/* Set the nesting depth of an element */
if (shadowNode.nodeType === 1) {
if (shadowNode.parentNode && shadowNode.parentNode.__depth) {
/*
We want the depth of the node in the original tree, which can
change when it's removed from its parent.
*/
shadowNode.__depth = (shadowNode.__removalCount || 0) + shadowNode.parentNode.__depth + 1;
} else {
shadowNode.__depth = 1;
}
}

/* Remove an element if nested too deeply to avoid mXSS */
if (shadowNode.__depth >= MAX_NESTING_DEPTH) {
_forceRemove(shadowNode);
}

/* Deep shadow DOM detected */
if (shadowNode.content instanceof DocumentFragment) {
shadowNode.content.__depth = shadowNode.__depth;
_sanitizeShadowDOM(shadowNode.content);
}

Expand Down Expand Up @@ -1374,8 +1402,27 @@ function createDOMPurify() {
continue;
}

/* Set the nesting depth of an element */
if (currentNode.nodeType === 1) {
if (currentNode.parentNode && currentNode.parentNode.__depth) {
/*
We want the depth of the node in the original tree, which can
change when it's removed from its parent.
*/
currentNode.__depth = (currentNode.__removalCount || 0) + currentNode.parentNode.__depth + 1;
} else {
currentNode.__depth = 1;
}
}

/* Remove an element if nested too deeply to avoid mXSS */
if (currentNode.__depth >= MAX_NESTING_DEPTH) {
_forceRemove(currentNode);
}

/* Shadow DOM detected, sanitize it */
if (currentNode.content instanceof DocumentFragment) {
currentNode.content.__depth = currentNode.__depth;
_sanitizeShadowDOM(currentNode.content);
}

Expand Down
2 changes: 1 addition & 1 deletion dist/purify.es.mjs.map

Large diffs are not rendered by default.

51 changes: 49 additions & 2 deletions dist/purify.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dist/purify.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/purify.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/purify.min.js.map

Large diffs are not rendered by default.

0 comments on commit 6ea80cd

Please sign in to comment.