Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extra DOM Clobbering protection via SANITIZE_NAMED_PROPS config #710

Merged
merged 1 commit into from Aug 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 6 additions & 1 deletion README.md
Expand Up @@ -43,7 +43,7 @@ let clean = DOMPurify.sanitize(dirty);
Or maybe this, if you love working with Angular or alike:

```js
import * as DOMPurify from 'dompurify'
import * as DOMPurify from 'dompurify';

let clean = DOMPurify.sanitize('<b>hello there</b>');
```
Expand Down Expand Up @@ -298,6 +298,11 @@ var clean = DOMPurify.sanitize(dirty, {WHOLE_DOCUMENT: true});
// disable DOM Clobbering protection on output (default is true, handle with care, minor XSS risks here)
var clean = DOMPurify.sanitize(dirty, {SANITIZE_DOM: false});

// enforce strict DOM Clobbering protection via namespace isolation (default is false)
// when enabled, isolates the namespace of named properties (i.e., `id` and `name` attributes)
// from JS variables by prefixing them with the string `user-content-`
var clean = DOMPurify.sanitize(dirty, {SANITIZE_NAMED_PROPS: true});

// keep an element's content when the element is removed (default is true)
var clean = DOMPurify.sanitize(dirty, {KEEP_CONTENT: false});

Expand Down
34 changes: 33 additions & 1 deletion 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.

34 changes: 33 additions & 1 deletion dist/purify.es.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.es.js.map

Large diffs are not rendered by default.

34 changes: 33 additions & 1 deletion 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.

32 changes: 31 additions & 1 deletion src/purify.js
Expand Up @@ -266,9 +266,27 @@ function createDOMPurify(window = getGlobal()) {
* case Trusted Types are not supported */
let RETURN_TRUSTED_TYPE = false;

/* Output should be free from DOM clobbering attacks? */
/* 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;

Expand Down Expand Up @@ -443,6 +461,7 @@ function createDOMPurify(window = getGlobal()) {
RETURN_TRUSTED_TYPE = cfg.RETURN_TRUSTED_TYPE || false; // Default false
FORCE_BODY = cfg.FORCE_BODY || false; // Default false
SANITIZE_DOM = cfg.SANITIZE_DOM !== false; // Default true
SANITIZE_NAMED_PROPS = cfg.SANITIZE_NAMED_PROPS || false; // Default false
KEEP_CONTENT = cfg.KEEP_CONTENT !== false; // Default true
IN_PLACE = cfg.IN_PLACE || false; // Default false
IS_ALLOWED_URI = cfg.ALLOWED_URI_REGEXP || IS_ALLOWED_URI;
Expand Down Expand Up @@ -1205,6 +1224,17 @@ function createDOMPurify(window = getGlobal()) {
continue;
}

/* Full DOM Clobbering protection via namespace isolation,
* Prefix id and name attributes with `user-content-`
*/
if (SANITIZE_NAMED_PROPS && (lcName === 'id' || lcName === 'name')) {
// Remove the attribute with this value
_removeAttribute(name, currentNode);

// Prefix the value and later re-create the attribute with the sanitized value
value = SANITIZE_NAMED_PROPS_PREFIX + value;
}

/* Handle attributes that require Trusted Types */
if (
trustedTypesPolicy &&
Expand Down
21 changes: 21 additions & 0 deletions test/test-suite.js
Expand Up @@ -465,6 +465,27 @@
['', '<form><input name="attributes"></form>', '<form><input></form>']
);
});
QUnit.test('Config-Flag tests: SANITIZE_NAMED_PROPS', function (assert) {
// SANITIZE_NAMED_PROPS
assert.equal(
DOMPurify.sanitize('<a id="x"></a>', {
SANITIZE_NAMED_PROPS: true,
}),
'<a id="user-content-x"></a>'
);
assert.equal(
DOMPurify.sanitize('<form id="x"><input id="y"></form>', {
SANITIZE_NAMED_PROPS: true,
}),
'<form id="user-content-x"><input id="user-content-y"></form>'
);
assert.equal(
DOMPurify.sanitize('<a id="x"></a><a id="x"></a>', {
SANITIZE_NAMED_PROPS: true,
}),
'<a id="user-content-x"></a><a id="user-content-x"></a>'
);
});
QUnit.test('Config-Flag tests: WHOLE_DOCUMENT', function (assert) {
//WHOLE_DOCUMENT
assert.equal(DOMPurify.sanitize('123', { WHOLE_DOCUMENT: false }), '123');
Expand Down