Technical Documentation: cure53/DOMPurify
โน๏ธ Provenance: Multi-Tier Ingestion ยท CodeWiki Reference
Overview (`README.md`)
DOMPurify
DOMPurify is a DOM-only, super-fast, uber-tolerant XSS sanitizer for HTML, MathML and SVG.
It's also very simple to use and get started with. DOMPurify was started in February 2014 and, meanwhile, has reached version v3.4.16.
DOMPurify runs as 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.
Note that DOMPurify v2.5.9 is the latest version supporting MSIE. For important security updates compatible with MSIE, please use the 2.x branch.
Our automated tests cover 9 browser/OS combinations on the current engines (Chromium, Firefox, and WebKit across Ubuntu, macOS, and Windows) on every push, and a separate matrix re-runs the suite on older engine snapshots (back to roughly Chromium 110, Firefox 108 and WebKit 16.4, around three years old) so regressions on outdated browsers get caught too. We also run Node.js v20, v22, v24, v25 and v26 with DOMPurify on jsdom. Older Node versions are known to work as well, but hey... no guarantees.
DOMPurify is written by security people who have vast background in web attacks and XSS. Fear not. For more details please also read about our Security Goals & Threat Model. Please, read it. Like, really. And if you enjoy the gory details, the Attack Classes & Bypass History page catalogs the parser-mutation, namespace, clobbering, and template tricks DOMPurify defends against.
The DOMPurify project inspired the creation of the HTML Sanitizer API, which is already shipping in many browsers. The same capability is now being standardized directly in the WHATWG HTML specification.
Table of Contents
- What does it do?
- How do I use it?
- Is there a demo?
- What if I find a security bug?
- Some purification samples please?
- What is supported?
- What about legacy browsers like Internet Explorer?
- What about DOMPurify and Trusted Types?
- Can I configure DOMPurify?
- Persistent Configuration
- Hooks
- Removed Configuration
- Continuous Integration
- Security Mailing List
- Who contributed?
What does it do?
DOMPurify sanitizes HTML and prevents XSS attacks. You can feed DOMPurify with e.g. a string full of dirty HTML and it will return a string (unless configured otherwise) with clean HTML. DOMPurify will strip out everything that contains dangerous HTML and thereby prevent XSS attacks and other nastiness. It's also damn bloody fast. We use the technologies the browser provides and turn them into an XSS filter. The faster your browser, the faster DOMPurify will be.
How do I use it?
It's easy. Just include DOMPurify on your website.
Using the unminified version (source-map available)
<script type="text/javascript" src="dist/purify.js"></script>Using the minified and tested production version (source-map available)
<script type="text/javascript" src="dist/purify.min.js"></script>Afterwards you can sanitize strings by executing the following code:
const clean = DOMPurify.sanitize(dirty);Or maybe this, if you love working with Angular or alike:
import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize('<b>hello there</b>');The resulting HTML can be written into a DOM element using innerHTML or the DOM using document.write(). That is fully up to you.
Note that by default, we permit HTML, SVG and MathML. If you only need HTML, which might be a very common use-case, you can easily set that up as well:
const clean = DOMPurify.sanitize(dirty, { USE_PROFILES: { html: true } });Is there any foot-gun potential?
Well, please note, if you first sanitize HTML and then modify it afterwards, you might easily void the effects of sanitization. If you feed the sanitized markup to another library after sanitization, please be certain that the library doesn't mess around with the HTML on its own. See the Security Goals & Threat Model for safe-usage recipes and the tags/attributes worth thinking twice about, and Attack Classes & Bypass History for why post-processing and changing the markup context defeat sanitization.
What about passing a DOM node instead of a string?
DOMPurify.sanitize() also accepts a DOM node (an Element, DocumentFragment or Document). Since 3.4.14 that path is hardened for nodes that did not come out of the HTML parser: a node built with the DOM API or parsed as XML/XHTML (for example via DOMParser with application/xhtml+xml and importNode()) can carry case-preserved attribute names such as ONERROR, or a rawtext element like <style> with an element child or its own end tag inside its text. Both shapes are invisible to a string sanitizer because the HTML parser can never build them, but they break out on reparse. DOMPurify now removes attributes by their exact Attr node and treats these literal-text trees as unsafe, so mixing document contexts on the input side is covered. It remains your job not to mix contexts on the output side, see the paragraph above.
Okay, makes sense, let's move on
After sanitizing your markup, you can also have a look at the property DOMPurify.removed and find out, what elements and attributes were thrown out. Please do not use this property for making any security critical decisions. This is just a little helper for curious minds.
Running DOMPurify on the server
DOMPurify technically also works server-side with Node.js. Our support strives to follow the Node.js release cycle.
Running DOMPurify on the server requires a DOM to be present, which is probably no surprise. Usually, 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.
Please also be aware that tools like 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. For background on why the server-side DOM you choose is part of your trusted computing base, see Attack Classes & Bypass History.
On runtimes where jsdom cannot run, such as Cloudflare Workers or Vercel Edge, purify-edge is a third-party project that runs an unmodified DOMPurify on a parse5-based window. Our test suite can be run against it with npm run test:purify-edge as a compatibility check. It is not maintained by us, and a passing run is neither an endorsement nor a security guarantee.
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:
npm install dompurify
npm install jsdomFor jsdom (please use an up-to-date version), this should do the trick:
const createDOMPurify = require('dompurify');
const { JSDOM } = require('jsdom');
const window = new JSDOM('').window;
const DOMPurify = createDOMPurify(window);
const clean = DOMPurify.sanitize('<b>hello there</b>');Or even this, if you prefer working with imports:
import { JSDOM } from 'jsdom';
import DOMPurify from 'dompurify';
const window = new JSDOM('').window;
const purify = DOMPurify(window);
const clean = purify.sanitize('<b>hello there</b>');If you have problems making it work in your specific setup, consider looking at the amazing isomorphic-dompurify project which solves lots of problems people might run into.
npm install isomorphic-dompurifyimport DOMPurify from 'isomorphic-dompurify';
const clean = DOMPurify.sanitize('<s>hello</s>');Is there a demo?
Of course there is a demo! Play with DOMPurify
What if I find a security bug?
First of all, please immediately contact us via email so we can work on a fix. PGP key
Also, you probably qualify for a bug bounty! The fine folks over at Fastmail use DOMPurify for their services and added our library to their bug bounty scope. So, if you find a way to bypass or weaken DOMPurify, please also have a look at their website and the bug bounty info.
Some purification samples please?
How does purified markup look like? Well, the demo shows it for a big bunch of nasty elements. But let's also show some smaller examples!
DOMPurify.sanitize('<img src=x onerror=alert(1)//>'); // becomes <img src="x">
DOMPurify.sanitize('<svg><g/onload=alert(2)//<p>'); // becomes <svg><g></g></svg>
DOMPurify.sanitize('<p>abc<iframe//src=jAva	script:alert(3)>def</p>'); // becomes <p>abc</p>
DOMPurify.sanitize('<math><mi//xlink:href="data:x,<script>alert(4)</script>">'); // becomes <math><mi></mi></math>
DOMPurify.sanitize('<TABLE><tr><td>HELLO</tr></TABL>'); // becomes <table><tbody><tr><td>HELLO</td></tr></tbody></table>
DOMPurify.sanitize('<UL><li><A HREF=//google.com>click</UL>'); // becomes <ul><li><a href="//google.com">click</a></li></ul>These are just a taste. For the full taxonomy of attack classes these samples come from - mutation XSS, namespace confusion, DOM clobbering, rawtext breakouts, and more - see Attack Classes & Bypass History.
What is supported?
DOMPurify currently supports HTML5, SVG and MathML. DOMPurify per default allows CSS, HTML custom data attributes. DOMPurify also supports the Shadow DOM - and sanitizes DOM templates recursively. That covers open shadow roots only: a closed shadow root cannot be read from script, so DOMPurify can neither see nor sanitize what is inside it. Strings are not affected, because DOMPurify's own parser ignores declarative shadow roots (<template shadowrootmode>), but setHTMLUnsafe() and Document.parseHTMLUnsafe() do not: if you parse untrusted markup with those and then pass the resulting nodes to DOMPurify (IN_PLACE, RETURN_DOM, RETURN_DOM_FRAGMENT), any closed shadow root in that markup comes back unsanitized. Pass untrusted markup to DOMPurify as a string instead. DOMPurify also allows you to sanitize HTML for being used with the jQuery $() and elm.html() API without any known problems. For the exact set of elements and attributes permitted by default, see the Default TAGs & ATTRIBUTEs allow-list & blocklist wiki page.
What about legacy browsers like Internet Explorer?
DOMPurify does nothing at all. It simply returns exactly the string that you fed it. DOMPurify exposes a property called isSupported, which tells you whether it will be able to do its job, so you can come up with your own backup plan.
What about DOMPurify and Trusted Types?
In version 1.0.9, support for the Trusted Types API (MDN) was added to DOMPurify.
In version 2.0.0, a config flag was added to control DOMPurify's behavior regarding this.
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.
window.trustedTypes.createPolicy('default', {
createHTML: (to_escape) =>
DOMPurify.sanitize(to_escape, { RETURN_TRUSTED_TYPE: false }),
});When no TRUSTED_TYPES_POLICY is supplied, DOMPurify attempts to create its own internal Trusted Types policy named dompurify. If your page already defines its own policy together with a strict CSP (for example trusted-types my-organization) that does not allow a policy named dompurify, this attempt is blocked by the browser and logs a TrustedTypes policy dompurify could not be created. warning along with a CSP violation.
To stop DOMPurify from creating its internal fallback policy, pass TRUSTED_TYPES_POLICY: null. This is the right choice when you call DOMPurify.sanitize from inside your own policy's createHTML, and it means you do not have to add dompurify to your CSP's trusted-types allowlist.
window.trustedTypes.createPolicy('my-organization', {
createHTML: (input) =>
DOMPurify.sanitize(input, { TRUSTED_TYPES_POLICY: null }),
});Do not pass your own wrapping policy back to DOMPurify as its TRUSTED_TYPES_POLICY (for example via DOMPurify.setConfig({ TRUSTED_TYPES_POLICY: myPolicy })) when that policy's createHTML already calls DOMPurify.sanitize. That is circular by definition - sanitizing would call the policy, which sanitizes by calling DOMPurify again - and DOMPurify will throw a descriptive TypeError to prevent the infinite recursion. Your own policy should call DOMPurify; DOMPurify should not be configured to call your policy.
If you want this default-policy pattern applied across an entire page automatically - so that every HTML sink is sanitized, including legacy code, third-party widgets, and the thousands of innerHTML assignments you cannot easily find or rewrite - have a look at DOMFortify. It installs exactly such a Trusted Types default policy backed by DOMPurify and refuses script sinks (eval, script.src, ...) outright. It is a deliberately separate project: DOMPurify stays a focused sanitizer, and DOMFortify handles the document-wide enforcement layer that is intentionally out of DOMPurify's scope.
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 folder to see a bunch of examples on how you can customize DOMPurify.
Before you widen the allow-list (ADD_TAGS, ADD_ATTR, CUSTOM_ELEMENT_HANDLING, โฆ) or relax a default, it's worth skimming the tags and attributes to think twice about - a few are dangerous in non-obvious ways.
General settings
// strip {{ ... }}, ${ ... } and <% ... %> to make output safe for template systems
// be careful please, this mode is not recommended for production usage.
// allowing template parsing in user-controlled HTML is not advised at all.
// only use this mode if there is really no alternative.
const clean = DOMPurify.sanitize(dirty, { SAFE_FOR_TEMPLATES: true });
// change how e.g. comments containing risky HTML characters are treated.
// be very careful, this setting should only be set to `false` if you really only handle
// HTML and nothing else, no SVG, MathML or the like.
// Otherwise, changing from `true` to `false` will lead to XSS in this or some other way.
const clean = DOMPurify.sanitize(dirty, { SAFE_FOR_XML: false });Control our allow-lists and block-lists
/* Detailed source-code truncated for AI context efficiency. */Control behavior relating to Custom Elements
/* Detailed source-code truncated for AI context efficiency. */Control behavior relating to URI values
// extend the existing array of elements that can use Data URIs
const clean = DOMPurify.sanitize(dirty, { ADD_DATA_URI_TAGS: ['a', 'area'] });
// extend the existing array of elements that are safe for URI-like values (be careful, XSS risk)
const clean = DOMPurify.sanitize(dirty, { ADD_URI_SAFE_ATTR: ['my-attr'] });Control permitted attribute values
// allow external protocol handlers in URL attributes (default is false, be careful, XSS risk)
// by default only http, https, ftp, ftps, tel, mailto, callto, sms, cid, xmpp and matrix are allowed.
const clean = DOMPurify.sanitize(dirty, { ALLOW_UNKNOWN_PROTOCOLS: true });
// allow specific protocol handlers in URL attributes via regex (default is false, be careful, XSS risk)
// by default only (protocol-)relative URLs, http, https, ftp, ftps, tel, mailto, callto, sms, cid, xmpp and matrix are allowed.
// Default RegExp: /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i;
// The example below extends the default with one additional scheme (sftp).
// Keep the pattern linear-time: it runs against attacker-controlled values.
const clean = DOMPurify.sanitize(dirty, {
ALLOWED_URI_REGEXP:
/^(?:(?:(?:f|ht)tps?|sftp|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i,
});Influence the return-type
// return a DOM HTMLBodyElement instead of an HTML string (default is false)
const clean = DOMPurify.sanitize(dirty, { RETURN_DOM: true });
// return a DOM DocumentFragment instead of an HTML string (default is false)
const clean = DOMPurify.sanitize(dirty, { RETURN_DOM_FRAGMENT: true });
// use the RETURN_TRUSTED_TYPE flag to turn on Trusted Types support if available
const clean = DOMPurify.sanitize(dirty, { RETURN_TRUSTED_TYPE: true }); // will return a TrustedHTML object instead of a string if possible
// use a provided Trusted Types policy
const clean = DOMPurify.sanitize(dirty, {
// supplied policy must define createHTML and createScriptURL
TRUSTED_TYPES_POLICY: trustedTypes.createPolicy('dompurify', {
createHTML(s) {
return s;
},
createScriptURL(s) {
return s;
},
}),
});
// opt out of DOMPurify's internal `dompurify` Trusted Types policy entirely
// (useful when your CSP `trusted-types` allowlist does not include `dompurify`)
const clean = DOMPurify.sanitize(dirty, { TRUSTED_TYPES_POLICY: null });Influence how we sanitize
/* Detailed source-code truncated for AI context efficiency. */Influence where we sanitize
// use the IN_PLACE mode to sanitize a node "in place", which is much faster depending on how you use DOMPurify
const dirty = document.createElement('a');
dirty.setAttribute('href', 'javascript:alert(1)');
const clean = DOMPurify.sanitize(dirty, { IN_PLACE: true }); // see https://github.com/cure53/DOMPurify/issues/288 for more infoA few things to know about IN_PLACE:
- The root node you pass in must itself be an allowed tag and must not be DOM-clobbered (for example a
<form>with a child namednodeNameorownerDocument). If it is, DOMPurify strips the root's subtree of every non-allow-listed attribute and then throws aTypeError, so a rejected root is never handed back armed. - If anything throws mid-walk, the same fail-closed neutralization runs over the root and over every subtree already detached during that walk before the error propagates.
- Nodes that a hook detaches from the tree (a common pattern, see Hooks) are treated as removed. In
IN_PLACEmode, every element that was under the root whensanitize()started and is no longer under it when the call ends has its non-allow-listed attributes stripped beforesanitize()returns or throws, no matter what detached it: a hook removing the node it was called for, its parent or another ancestor, or a node elsewhere in the tree, a hook moving a subtree out of the root, or a custom element reacting to one of DOMPurify's own attribute removals. So an `` that was already loading when you built the live tree cannot fire aftersanitize()returns. Likewise, a node of your tree that a hook or custom element moves to a position the walk has already passed is still sanitized beforesanitize()returns; if the tree keeps changing that way, DOMPurify strips the root bare and throws instead of returning it. - DOMPurify cannot undo engine mutations that already fired before
sanitize()was called (a patch applied on connection, aselectedcontentre-clone, and so on). Sanitize attacker-controlled trees before connecting them to the live document, not after.
There is even more examples here, showing how you can run, customize and configure DOMPurify to fit your needs.
Persistent Configuration
Instead of repeatedly passing the same configuration to DOMPurify.sanitize, you can use the DOMPurify.setConfig method. Your configuration will persist until your next call to DOMPurify.setConfig, or until you invoke DOMPurify.clearConfig to reset it. Remember that there is only one active configuration, which means once it is set, all extra configuration parameters passed to DOMPurify.sanitize are ignored.
Hooks
DOMPurify allows you to augment its functionality by attaching one or more functions with the DOMPurify.addHook method to one of the following hooks:
beforeSanitizeElementsuponSanitizeElement(No 's' - called for every element)afterSanitizeElementsbeforeSanitizeAttributesuponSanitizeAttributeafterSanitizeAttributesbeforeSanitizeShadowDOMuponSanitizeShadowNodeafterSanitizeShadowDOM
It passes the currently processed DOM node, when needed a literal with verified node and attribute data and the DOMPurify configuration to the callback. Check out the MentalJS hook demo to see how the API can be used nicely.
Example:
DOMPurify.addHook(
'uponSanitizeAttribute',
function (currentNode, hookEvent, config) {
// 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
}
);Hook behavior worth knowing
- Detaching a node from a hook is supported. If a
beforeSanitizeElementsoruponSanitizeElementhook removes the current node from the tree (for examplenode.remove()to drop aforeignObject), DOMPurify treats the node as removed and stops processing it. Such nodes are not recorded inDOMPurify.removed. InIN_PLACEmode the detached subtree is still neutralized (since 3.4.13), because a live node may carry an already-queued resource event. afterSanitizeElementsruns for kept custom elements, too. Since 3.4.12, an element admitted viaCUSTOM_ELEMENT_HANDLING.tagNameCheckgoes throughafterSanitizeElementsexactly like an allow-listed element, so a policy applied in that hook (for example stripping an attribute from every surviving element) cannot silently skip custom elements (GHSA-c2j3-45gr-mqc4).- Prefer
hookEvent.keepAttr/forceKeepAttrover writing tohookEvent.allowedAttributesorallowedTags. The per-node flags cannot leak. Writes to the allow-list objects are isolated per call, including when the hook is installed lazily from inside another hook and when a persistent config fromsetConfig()is active (GHSA-cmwh-pvxp-8882), but they remain the sharper tool. afterSanitize*hooks run after validation. Whatever you write there is not re-checked. Put attacker-influenced values throughuponSanitize*hooks instead.
A note on calling `sanitize()` from a hook
DOMPurify.sanitize() is not re-entrant. Please do not call it from inside a hook, or from a configuration callback such as CUSTOM_ELEMENT_HANDLING.tagNameCheck or attributeNameCheck. Those callbacks run in the middle of an active sanitizer pass.
A nested sanitize() call re-reads the configuration handed to it and, in doing so, replaces the configuration the outer pass is still using. The rest of the outer document is then sanitized against the nested call's configuration instead of yours. Since the nested call typically runs with the default configuration, a strict ALLOWED_TAGS allow-list can silently widen back to the default one part-way through a document, with no error and no warning.
If you need to sanitize nested markup, for example an HTML fragment carried inside an attribute value, you have two safe options. Either set your configuration once with DOMPurify.setConfig instead of passing it per call, since a persistent configuration is shared by the nested call and stays in effect for the whole pass; or collect the fragments during the hook and sanitize them with a separate sanitize() call after the outer one has returned.
Removed Configuration
| Option | Since | Note |
|---|---|---|
| SAFE_FOR_JQUERY | 2.1.0 | No replacement required. |
Continuous Integration
We are currently using GitHub Actions in combination with Playwright. This lets us confirm on every commit that everything works in the relevant modern browsers, and a separate scheduled and on-merge workflow re-runs the suite on older engine snapshots so breakage on outdated browsers is caught too. Check out the build logs here: https://github.com/cure53/DOMPurify/actions
You can further run local tests by executing npm run test.
All relevant commits will be signed with the key 0x24BB6BF4 for additional security (since 8th of April 2016).
Development and contributing
Installation (`npm i`)
We support npm officially. GitHub Actions workflow is configured to install dependencies using npm. When using a deprecated version of npm, we cannot fully ensure the versions of installed dependencies, which might lead to unanticipated problems.
Scripts
We use ESLint via xo as part of our pre-commit workflow to help ensure code consistency. In addition, we use Prettier for source and Markdown formatting, and /dist assets are built through rolldown.
These are our npm scripts:
npm run devto build the unminified UMD bundle while watching sources for changesnpm run testto lint the sources, run tests through jsdom, and run browser tests in Chromium via Playwrightnpm run test:jsdomto only run tests through jsdomnpm run test:happydomto run the suite through happy-dom (an unsupported environment; kept as a robustness check, not a compatibility promise)npm run test:purify-edgeto run the suite through purify-edge, a third-party parse5-based window (an unsupported environment; kept as a compatibility check, not a promise of support). The XSS sink tests run in a second pass, with jsdom as the sinknpm run test:browserto only run tests through Playwrightnpm run test:browser:legacyto run the suite on older browser engines (pointPW_MODULEat a pinned old Playwright install; see.github/workflows/legacy-browsers.yml)npm run test:cito run the CI test flow for jsdom and Playwrightnpm run test:fuzzto run a small fuzzer coveringsanitize()and CONFIG
npm run benchto run the jsdom micro-benchmark over the builtdist/purify.cjs(build first;--jsonand--compare a.json b.jsonsupport A/B runs across branches - results are directional, confirm user-facing claims in real browsers)npm run coverageto build an instrumented bundle, run the jsdom suite, and write a local HTML line/branch coverage report tocoverage/index.html(jsdom scope only, not run in CI)npm run build:covto only build the instrumented coverage bundle
npm run lintto lint the sources using ESLint via xonpm run formatto format JavaScript/TypeScript and Markdown sources with Prettiernpm run format:jsto only format JavaScript/TypeScript sourcesnpm run format:mdto only format Markdown files
npm run buildto build type declarations and distribution bundles, then fix and clean up generated typesnpm run build:typesto only emit TypeScript declaration filesnpm run build:rolldownto build all Rolldown bundlesnpm run build:umdto only build an unminified UMD bundlenpm run build:umd:minto only build a minified UMD bundlenpm run build:esto only build the ES module bundlenpm run build:cjsto only build the CommonJS bundlenpm run build:fix-typesto post-process generated type filesnpm run build:cleanupto clean up temporary generated type output
npm run verify-typescriptto run the TypeScript verification scriptnpm run commit-amend-buildto run the maintainer helper script for amending build output
Note: all run scripts triggered via npm run <script>.
There are more npm scripts but they are mainly to integrate with CI or are meant to be "private" for instance to amend build distribution files with every commit.
Security Mailing List
We maintain a mailing list that notifies whenever a security-critical release of DOMPurify was published. This means, if someone found a bypass and we fixed it with a release (which always happens when a bypass was found) a mail will go out to that list. This usually happens within minutes or a few hours after learning about a bypass. The list can be subscribed to here:
https://lists.ruhr-uni-bochum.de/mailman/listinfo/dompurify-security
Feature releases will not be announced to this list.
Who contributed?
Many people have helped DOMPurify become what it is today, and they deserve to be acknowledged!
gnyselcuk, leechristensen,offset, Bankde, lukewarlow, DEMON1A, fg0x0, kodareef5, DavidOliver, 1Jesper1, bencalif, trace37labs, eddieran, christos-eth, researchatfluidattacks, frevadiscor, Rotzbua, binhpv, MariusRumpf, prasadrajandran, Cybozu ๐๐ธ, hata6502 ๐ธ, openclaw ๐ธ, intra-mart-dh ๐ธ, nelstrom โค๏ธ, hash_kitten โค๏ธ, kevin_mizu โค๏ธ, icesfont โค๏ธ, reduckted โค๏ธ, dcramer ๐ธ, JGraph ๐ธ, baekilda ๐ธ, Healthchecks ๐ธ, Sentry ๐ธ, jarrodldavis ๐ธ, CynegeticIO, ssi02014 โค๏ธ, GrantGryczan, Lowdefy, granlem, oreoshake, tdeekens โค๏ธ, peernohell โค๏ธ, is2ei, SoheilKhodayari, franktopel, NateScarlet, neilj, fhemberger, Joris-van-der-Wel, ydaniv, terjanq, filedescriptor, ConradIrwin, gibson042, choumx, 0xSobky, styfle, koto, tlau88, strugee, oparoz, mathiasbynens, edg2s, dnkolegov, dhardtke, wirehead, thorn0, styu, mozfreddyb โค๏ธ, mikesamuel, jorangreef, jimmyhchan, jameydeorio, jameskraus, hyderali, hansottowirtz, hackvertor, freddyb, flavorjones, djfarrelly, devd, camerondunford, buu700, buildog, alabiaga, Vector919, Robbert, GreLI, FuzzySockets, ArtemBernatskyy, @garethheyes, @shafigullin, @mmrupp, @irsdl,ShikariSenpai, ansjdnakjdnajkd, @asutherland, @mathias, @cgvwzq, @robbertatwork, @giutro, @CmdEngineer_, @avr4mit, davecardwell, Develop-KIM, asamuzaK, fishjojo1 โค๏ธ, Rikuxx0, donmccurdy, hhk-png, elrion018, michalnieruchalski-tiugo, reey, KanhaKanhaiya, odaysec, Akokonunes, alirezarouhbakhsh, Jaybhade and especially @securitymb โค๏ธ & @masatokinugawa โค๏ธ
Overview (`demos/README.md`)
What is this?
This is a collection of demos of how to use DOMPurify. When run without any additional configuration parameters, DOMPurify will reliably protect against XSS and DOM Clobbering attacks. But there is a lot of more things this library can do. For example proxy all HTTP resources, enhance accessibility, prevent links to open in the same window, allow and sand-box JavaScript and more.
This collection of demos shows sample code for several different ways you can use DOMPurify. Please feel free to suggest additional demos if needed. All demos we have collected so far will be shown and explained below.
Basic Demo [Link](basic-demo.html)
This is the most basic of all demos. It shows how you use DOMPurify and that's it. No configuration, no hooks, no extras. Just DOMPurify running with default settings.
This is the relevant code:
// Clean HTML string and write into our DIV
const clean = DOMPurify.sanitize(dirty);Config Demo [Link](config-demo.html)
This demo shows how to use the configuration object the right way. In this demo, we only permit elements and we want to preserve their text, but not the content of nested elements inside the.
This is the relevant code:
// Specify a configuration directive, only <P> elements allowed
// Note: We want to also keep <p>'s text content, so we add #text too
const config = { ALLOWED_TAGS: ['p', '#text'], KEEP_CONTENT: false };
// Clean HTML string and write into our DIV
const clean = DOMPurify.sanitize(dirty, config);Advanced Config Demo [Link](advanced-config-demo.html)
This demo shows, how we can use the configuration object to instruct DOMPurify to be more specific with what is to be permitted and what is not. We want to permit `` elements and the fictional <ying> and <yang> tag. We also want to allow the kitty-litter attribute because why not - and make sure that a document-object is returned after sanitation - and not a plain string.
This is the relevant code:
// Specify a configuration directive
const config = {
ALLOWED_TAGS: ['p', '#text'], // only <P> and text nodes
KEEP_CONTENT: false, // remove content from non-allow-listed nodes too
ADD_ATTR: ['kitty-litter'], // permit kitty-litter attributes
ADD_TAGS: ['ying', 'yang'], // permit additional custom tags
RETURN_DOM: true, // return a document object instead of a string
};
// Clean HTML string and write into our DIV
const clean = DOMPurify.sanitize(dirty, config);Hooks Demo [Link](hooks-demo.html)
DOMPurify allows you to use hooks. Hooks are basically scripts that can hook into certain parts of the DOMPurify code flow and do stuff. Stuff that you like to be done. By using hooks, you can literally make DOMPurify do whatever. To show you, how powerful and easy to use hooks are, we created some demos for you. Like this one, that essentially renders all tag content to be in capitals.
This is the relevant code:
// Add a hook to convert all text to capitals
DOMPurify.addHook('beforeSanitizeAttributes', function (node) {
// Set text node content to uppercase
if (node.nodeName && node.nodeName === '#text') {
node.textContent = node.textContent.toUpperCase();
}
});
// Clean HTML string and write into our DIV
const clean = DOMPurify.sanitize(dirty);Add hooks and remove hooks [Link](hooks-removal-demo.html)
A DOMPurify hook can also be removed in case you first need it and then you want to get rid of it right afterwards. This demo shows how you do that with ease and elegance.
This is the relevant code:
// Add a hook to convert all text to capitals
DOMPurify.addHook('beforeSanitizeAttributes', function (node) {
// Set text node content to uppercase
if (node.nodeName && node.nodeName === '#text') {
node.textContent = node.textContent.toUpperCase();
}
});
// Clean HTML string and write into our DIV
let clean = DOMPurify.sanitize(dirty);
// now let's remove the hook again
console.log(DOMPurify.removeHook('beforeSanitizeAttributes'));
// Clean HTML string and write into our DIV
let clean = DOMPurify.sanitize(dirty);Hook to remove empty elements [Link](hooks-node-removal-demo.html)
This demo shows how to use the uponSanitizeElement hook to remove elements that have no child nodes and no text content, cleaning empty leftovers out of the sanitized markup.
This is the relevant code:
DOMPurify.addHook('uponSanitizeElement', (node) => {
if (!node.hasChildNodes() && !node.textContent) {
node.remove();
}
});Hook to remove an element and its content [Link](hooks-node-removal2-demo.html)
ALLOWED_TAGS alone strips a tag but keeps its content, and FORBID_TAGS cannot be combined with ALLOWED_TAGS. To remove an element including everything inside it, allow the tag and remove the node in a hook. Note that the node must be on the allow-list for the hook to see it.
This is the relevant code:
DOMPurify.setConfig({ ALLOWED_TAGS: ['div', '#text', 'footer'] });
DOMPurify.addHook('uponSanitizeElement', (node) => {
if (node.tagName === 'FOOTER') {
node.remove();
}
});Hook to sanitize CSS [Link](hooks-sanitize-css-demo.html)
This demo walks the CSS rules of permitted <style> elements inside a hook and removes declarations that are not on a small property allow-list, so even allow-listed style content cannot smuggle in unwanted CSS.
Trusted Types demo [Link](trusted-types-demo.html)
This demo shows DOMPurify under a trusted-types Content Security Policy: the data-tt-policy-suffix attribute, RETURN_TRUSTED_TYPE: true, and wrapping DOMPurify in a policy of your own. It needs a browser with Trusted Types support to do anything.
Hook to open all links in a new window [Link](hooks-target-blank-demo.html)
This hook is an important one and used quite commonly. It is made to assure that all elements that can function as a link will open the linked page in a new tab or window. This is often of great importance in web mailers and other tools, where a click on a link is not supposed to navigate the original page but rather open another window or tab.
This is the relevant code:
// Add a hook to make all links open a new window
DOMPurify.addHook('afterSanitizeAttributes', function (node) {
// set all elements owning target to target=_blank
if ('target' in node) {
node.setAttribute('target', '_blank');
}
// set non-HTML/MathML links to xlink:show=new
if (
!node.hasAttribute('target') &&
(node.hasAttribute('xlink:href') || node.hasAttribute('href'))
) {
node.setAttribute('xlink:show', 'new');
}
});
// Clean HTML string and write into our DIV
const clean = DOMPurify.sanitize(dirty);Hook to white-list safe URI Schemes [Link](hooks-scheme-allowlist.html)
Depending on where you show your sanitized HTML, different URI schemes might cause trouble. And in most situations, you only want to allow HTTP and HTTPS - but not any of those fancy URI schemes supported on mobile devices or even on the desktop with Windows 10. This hook demo shows how to easily make sure only HTTP, HTTPS and FTP URIs are permitted while all others are eliminated for good.
Note that you might want to be more thorough, if not only links but also backgrounds and other attributes should be covered. We have an example later on to cover all these too.
This is the relevant code:
// allowed URI schemes
const allowlist = ['http', 'https', 'ftp'];
// build fitting regex
const regex = RegExp('^(' + allowlist.join('|') + '):', 'gim');
// Add a hook to enforce URI scheme allow-list
DOMPurify.addHook('afterSanitizeAttributes', function (node) {
// build an anchor to map URLs to
const anchor = document.createElement('a');
// check all href attributes for validity
if (node.hasAttribute('href')) {
anchor.href = node.getAttribute('href');
if (anchor.protocol && !anchor.protocol.match(regex)) {
node.removeAttribute('href');
}
}
// check all action attributes for validity
if (node.hasAttribute('action')) {
anchor.href = node.getAttribute('action');
if (anchor.protocol && !anchor.protocol.match(regex)) {
node.removeAttribute('action');
}
}
// check all xlink:href attributes for validity
if (node.hasAttribute('xlink:href')) {
anchor.href = node.getAttribute('xlink:href');
if (anchor.protocol && !anchor.protocol.match(regex)) {
node.removeAttribute('xlink:href');
}
}
});
// Clean HTML string and write into our DIV
const clean = DOMPurify.sanitize(dirty);Hook to allow and sand-box all JavaScript [Link](hooks-mentaljs-demo.html)
Okay, now this is real witch-craft! Imagine you want users to submit JavaScript but it should be sand-boxed. With a hook, you can actually do that. What we are doing here is permitting all JavaScript and event handlers, but take their contents and sand-box it using Gareth Heyes' amazing MentalJS library. The hook shows how do to this safely.
Be careful though, this is playing with fire. If you want to use this in production, better give us a quick ping to see if all is really working as desired.
This is the relevant code:
// allow script elements
const config = {
ADD_TAGS: ['script'],
ADD_ATTR: ['onclick', 'onmouseover', 'onload', 'onunload'],
};
// Add a hook to sanitize all script content with MentalJS
DOMPurify.addHook('uponSanitizeElement', function (node, data) {
if (data.tagName === 'script') {
let script = node.textContent;
if (
!script ||
'src' in node.attributes ||
'href' in node.attributes ||
'xlink:href' in node.attributes
) {
return node.parentNode.removeChild(node);
}
try {
let mental = MentalJS().parse({
options: {
eval: false,
dom: true,
},
code: script,
});
return (node.textContent = mental);
} catch (e) {
return node.parentNode.removeChild(node);
}
}
});
// Add a hook to sanitize all white-listed events with MentalJS
DOMPurify.addHook('uponSanitizeAttribute', function (node, data) {
if (data.attrName.match(/^on\w+/)) {
let script = data.attrValue;
try {
return (data.attrValue = MentalJS().parse({
options: {
eval: false,
dom: true,
},
code: script,
}));
} catch (e) {
return (data.attrValue = '');
}
}
});
// Clean HTML string and write into our DIV
const clean = DOMPurify.sanitize(dirty, config);Hook to proxy all links [Link](hooks-link-proxy-demo.html)
DOMPurify itself permits links to all resources that don't cause XSS. That includes pretty much all URI schemes and of course HTTP and HTTPS links. Yet, often, preventing XSS is not everything you want to do. And a common use case for a sanitizer is to also proxy all existing links on a website to make sure a de-referrer is used or the website owner has more control over what links are pointing where - to avoid referrer leaks, attacks using window.opener and alike. This hook shows, how all out-bound links can easily be proxied for maximum safety.
This is the relevant code:
// Add a hook to make all links point to a proxy
DOMPurify.addHook('afterSanitizeAttributes', function (node) {
// proxy form actions
if ('action' in node) {
node.setAttribute(
'action',
proxy + encodeURIComponent(node.getAttribute('action'))
);
}
// proxy regular HTML links
if (node.hasAttribute('href')) {
node.setAttribute(
'href',
proxy + encodeURIComponent(node.getAttribute('href'))
);
}
// proxy SVG/MathML links
if (node.hasAttribute('xlink:href')) {
node.setAttribute(
'xlink:href',
proxy + encodeURIComponent(node.getAttribute('xlink:href'))
);
}
});
// Clean HTML string and write into our DIV
const clean = DOMPurify.sanitize(dirty);Hook to proxy all HTTP leaks including CSS [Link](hooks-proxy-demo.html)
Now this is a hook you don't talk about on your first date. This monster has the purpose of proxying all known HTTP leaks including the ones hidden in CSS. It proxies HTML, CSS rules, in-line styles, @media rules, @font-face rules and @keyframes. It further eliminates @charset and @import as they both help carrying out XSS attacks. This hook is supposed to be a comprehensive demo on how to cover each and every HTTP leak. That's why the amount of code is a bit higher than usual.
When ever you want to reliably filter HTML and CSS using DOMPurify, this is the way to go. How to try the demo? Run the HTML file linked above in your browser and have a look at the network tab of your favorite debugger. You will see that all external resources are proxied. If not then you found a bug.
This is the relevant code:
/* Detailed source-code truncated for AI context efficiency. */Hook to sanitize SVGs shown via an `` tag. [Link](hooks-svg-demo.html)
DOMPurify can be used to sanitize SVGs, but there can be some issues with some of their content and that can be solved via custom hooks and parsing. Also, it's possible to allow some tags which are disabled by default when showing SVGs via an `` tag.
Here is an example which works well for content generated by Illustrator:
// Add a hook to post-process the sanitized SVG root: re-add the
// namespaces (added by tools like Illustrator, stripped during
// sanitization) that an <img>-embedded SVG needs to render
DOMPurify.addHook('afterSanitizeAttributes', function (node) {
if (node.tagName && node.tagName.toLowerCase() === 'svg') {
node.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
node.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
}
});
// Clean the SVG string and allow the "filter" tag
const clean = DOMPurify.sanitize(dirty, { ADD_TAGS: ['filter'] });
// Show the sanitized content in an <img> element
const img = new Image();
img.src = 'data:image/svg+xml;base64,' + window.btoa(clean);
document.getElementById('sanitized').appendChild(img);Overview (`typescript/commonjs-with-no-types/types/readme.md`)
This simulates not having @types/trusted-types installed.
Overview (`typescript/esm-with-no-types/types/readme.md`)
This simulates not having @types/trusted-types installed.
--- METRICS ---
- Files Extracted: 4
- Estimated Token Budget: ~13047 tokens
- Canonical Reference: https://codewiki.google/github.com/cure53/DOMPurify