DOMPurify is a widely used DOM-based library for sanitizing various markup languages against cross-site scripting (XSS) attacks. However, certain configuration options can weaken or completely bypass its protective mechanisms, rendering the sanitization ineffective.
Adding dangerous tags or attributes to the DOMPurify allowlist, disabling
critical sanitization features like SAFE_FOR_XML or SANITIZE_DOM, or
enabling risky options like ALLOW_UNKNOWN_PROTOCOLS can allow malicious
scripts to pass through the sanitizer and execute in the user’s browser.
The following code is vulnerable because DOMPurify is configured with options that weaken its sanitization before injecting content into a React component.
import DOMPurify from 'dompurify';
function Comment({ html }) {
const clean = DOMPurify.sanitize(html, {
ADD_TAGS: ['script', 'iframe'], // Noncompliant
ADD_ATTR: ['onclick', 'onerror'], // Noncompliant
});
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}
import DOMPurify from 'dompurify';
function Comment({ html }) {
const clean = DOMPurify.sanitize(html, {
USE_PROFILES: { html: true },
ADD_TAGS: ['custom-element'],
ADD_ATTR: ['data-custom'],
});
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}
The compliant solution restricts DOMPurify to HTML-only using USE_PROFILES
and only adds safe custom elements and data attributes to the allowlist. It
avoids allowing any tags or attributes that can execute JavaScript.
Here is a list of the exhaustive checks you need to do:
| Option | Risk | Best practice |
|---|---|---|
|
DOM XSS via executable elements |
Never allow |
|
DOM XSS via event handlers |
Never allow |
|
Mutation XSS (mXSS) |
Keep set to |
|
DOM Clobbering |
Keep set to |
|
|
Keep set to |
|
Dangerous head elements |
Keep set to |
|
Trusted Types bypass |
Keep set to |
|
1-click XSS via |
Never add URI-carrying attributes ( |
|
URI allowlist bypass via partial regex match (e.g. |
Always anchor the pattern with |
The following code is vulnerable because DOMPurify is configured with options that weaken its sanitization before sending content in an Express response.
const createDOMPurify = require('dompurify');
const { JSDOM } = require('jsdom');
const DOMPurify = createDOMPurify(new JSDOM('').window);
app.post('/comment', (req, res) => {
const clean = DOMPurify.sanitize(req.body.comment, {
ADD_TAGS: ['script', 'iframe'], // Noncompliant
ADD_ATTR: ['onclick', 'onerror'], // Noncompliant
});
res.send(`<div>${clean}</div>`);
});
const createDOMPurify = require('dompurify');
const { JSDOM } = require('jsdom');
const DOMPurify = createDOMPurify(new JSDOM('').window);
app.post('/comment', (req, res) => {
const clean = DOMPurify.sanitize(req.body.comment, {
USE_PROFILES: { html: true },
ADD_TAGS: ['custom-element'],
ADD_ATTR: ['data-custom'],
});
res.send(`<div>${clean}</div>`);
});
The compliant solution restricts DOMPurify to HTML-only using USE_PROFILES
and only adds safe custom elements and data attributes to the allowlist. It
avoids allowing any tags or attributes that can execute JavaScript.
Here is a list of the exhaustive checks you need to do:
| Option | Risk | Best practice |
|---|---|---|
|
DOM XSS via executable elements |
Never allow |
|
DOM XSS via event handlers |
Never allow |
|
Mutation XSS (mXSS) |
Keep set to |
|
DOM Clobbering |
Keep set to |
|
|
Keep set to |
|
Dangerous head elements |
Keep set to |
|
Trusted Types bypass |
Keep set to |
|
1-click XSS via |
Never add URI-carrying attributes ( |
|
URI allowlist bypass via partial regex match (e.g. |
Always anchor the pattern with |
When running DOMPurify server-side, always use the latest version of jsdom.
Older versions of jsdom have known bugs that can result in XSS even when
DOMPurify works correctly. Never use happy-dom as an alternative, as it is
not considered safe for sanitization purposes.