DOMPurify Template Bypass: Update Your Sanitizer

By Bug Circuit Security Team
DOMPurify Template Bypass: Update Your Sanitizer

If you use DOMPurify between versions 3.4.4 and 3.4.16, update to 3.4.17 or newer, because a critical bug lets template expressions slip through when you combine SAFE_FOR_TEMPLATES: true with a DOM output mode.

This post is for site owners, indie SaaS founders and developers who use (or whose developer uses) DOMPurify to clean up user-supplied HTML. You will learn what the flaw is in plain English, who is actually affected, how to check your own project in a few minutes, and the exact steps to fix it. The details come from the GitHub Advisory Database entry GHSA-cg2g-xh64-v4q7, published 2026-10-22 and rated critical.

What DOMPurify does, in plain English

DOMPurify is a popular JavaScript library that sanitizes HTML. "Sanitizing" means taking text that might contain something nasty, like a hidden <script> tag from a comment form, and stripping out the dangerous parts before you show it on a page. That nasty content is the raw material for cross-site scripting (XSS), an attack where someone else's code runs inside your visitors' browsers.

Sanitizers are a common last line of defense against XSS. OWASP, the open web security nonprofit, covers this in its XSS Prevention Cheat Sheet. The project itself lives at github.com/cure53/DOMPurify. When a sanitizer has a hole, anything that relies on it inherits the hole.

What the flaw is

DOMPurify has an option called SAFE_FOR_TEMPLATES. Some front-end frameworks and template engines treat special markers inside text as instructions to run. Common markers look like {{ ... }}, ${ ... } and <% ... %>. If user text containing those markers reaches a template engine that evaluates them, the text can turn into running code. Turning on SAFE_FOR_TEMPLATES tells DOMPurify to scrub those markers out as a safety net.

The advisory says the gap shows up when you use SAFE_FOR_TEMPLATES together with a DOM-returning mode. These modes hand back a live DOM object instead of a plain string:

  • RETURN_DOM: true
  • RETURN_DOM_FRAGMENT: true
  • IN_PLACE: true

The advisory points to a final cleanup pass (an internal function named _scrubTemplateExpressions). According to the advisory, that pass was already taught to look inside <template> content, but it never looks inside an attached shadow root. A shadow root is a private, hidden chunk of page content that a web component can attach to an element. It is a normal browser feature.

So an attacker who can supply a DOM node that carries a shadow root can split a template expression into pieces. The pieces sit in separate text nodes, with disallowed elements between them. Our reading of the advisory: once those elements are removed, the pieces end up side by side as a complete expression, and the cleanup pass never gets a chance to scrub them. The advisory text available to us is cut off at that point, so read the full entry for the finer details.

Is this dangerous for my site?

It depends on your setup, and the advisory is specific about the conditions. Here is a quick way to sort yourself.

Your situationWhat to do
dompurify 3.4.4 to 3.4.16, with SAFE_FOR_TEMPLATES: true and RETURN_DOM, RETURN_DOM_FRAGMENT or IN_PLACEThis is the case the advisory describes. Update as soon as you can, ideally today.
dompurify 3.4.4 to 3.4.16, but you use the default string outputThe advisory describes DOM output modes. Still update, it is a one-line change.
dompurify 3.4.4 to 3.4.16, SAFE_FOR_TEMPLATES not setThe advisory is about that option, so you are likely not exposed through it, but updating is still the simplest way to be sure.
dompurify 3.4.17 or newerPatched for this advisory.
Versions older than 3.4.4Not listed as affected by this advisory. Update to the latest anyway, since older versions may carry other known bugs.

The real-world risk also depends on what happens after sanitizing. The leftover expression only matters if something later evaluates it, such as a template-compiling framework rendering that content. Plain static pages are less exposed than apps that re-render user content through a template engine. We are not going to guess at your risk beyond that. Treat it as urgent for anything matching the first row.

How to check if you are affected

You need to answer two questions: which DOMPurify version do you run, and do you use the risky options?

Step 1: find your version

Run one of these in your project folder:

npm ls dompurify
pnpm why dompurify
yarn why dompurify

Look at every copy listed. DOMPurify often arrives as a dependency of another package, so you may see it nested several levels deep. Any copy from 3.4.4 up to 3.4.16 is in the affected range.

If you load DOMPurify from a CDN with a <script> tag, check the version number in the URL. If you copied a file into your own project, look at the version comment at the top of the file.

Step 2: search for the risky options

Search your whole source tree for the settings from the advisory. Run this from your project root. It skips node_modules and covers folders like src/, app/, components/, lib/ and pages/:

grep -rn "SAFE_FOR_TEMPLATES" --include='*.js' --include='*.ts' --include='*.jsx' --include='*.tsx' --include='*.vue' --exclude-dir=node_modules .
grep -rnE "RETURN_DOM|IN_PLACE" --include='*.js' --include='*.ts' --include='*.jsx' --include='*.tsx' --include='*.vue' --exclude-dir=node_modules .

If the first search finds nothing, your own code does not turn that option on in the folders you searched, though a wrapper library or dependency could still set it. If you find both SAFE_FOR_TEMPLATES: true and one of the DOM modes in the same configuration, you match the advisory.

Not a developer? Send your developer or agency the package name, dompurify, and the advisory ID, GHSA-cg2g-xh64-v4q7. They can answer both questions quickly.

The fix: update to 3.4.17 or newer

The advisory lists 3.4.17 as the first patched version. Here are the exact steps.

  1. Update the package. The caret range keeps you on the patched 3.x line and avoids an unexpected jump to a new major version:
npm install dompurify@^3.4.17
  1. Confirm what you now have:
npm ls dompurify
  1. If another package pulls in its own older copy, force a safe version in package.json using npm's overrides field, then reinstall:
"overrides": {
  "dompurify": "^3.4.17"
}
  1. Rebuild, run your tests, and deploy. Open a page that shows user-supplied content and make sure it still looks right.
  2. Commit the updated lockfile (package-lock.json, pnpm-lock.yaml or yarn.lock) so the fix sticks on every future install.

On pnpm, use the pnpm.overrides field; on Yarn, use resolutions. If you use a CDN link, change the version in the URL to 3.4.17 or later, and re-test.

Check the DOMPurify releases page for the changelog if you want to see what else changed in your upgrade.

If you cannot update today

Sometimes a release freeze or a locked dependency tree gets in the way. Based on the conditions in the advisory, here are stopgaps that reduce exposure. These are temporary, not replacements for the update.

  • Avoid combining SAFE_FOR_TEMPLATES: true with RETURN_DOM, RETURN_DOM_FRAGMENT or IN_PLACE until you can upgrade. The default string output is not the mode the advisory describes.
  • Do not pass sanitized user content through a template engine that evaluates {{ }}, ${ } or <% %> markers.
  • Do not accept DOM nodes with shadow roots from untrusted sources. Where possible, sanitize HTML strings instead.

Test any change to your configuration before shipping it, because it can change how your content renders.

Add a second layer so one bug is not the whole story

A sanitizer is one layer, not the whole wall. Layering helps when one piece has a bug, as this one does.

  • Content Security Policy (CSP). This is a browser rule set that limits which scripts can run on your pages. A well-built policy can blunt the impact of an XSS bug that slips past your sanitizer. You can test your headers with our security headers tool.
  • Escape on output. Treat user content as text by default and only allow HTML where you truly need it.
  • Keep dependencies current. npm audit flags known issues from the advisory database. Run it in your build, and again before each release.

Why a bug like this is easy to miss

The advisory describes a safety net that was extended to cover one place (<template> content) but not a similar one (shadow roots). Fixes tend to grow one case at a time, and an unusual feature combination can fall through. That is also why automated scanners struggle with this class of problem: they can see an outdated version number, but they cannot tell whether your particular configuration and rendering flow make it exploitable. We explain that difference in manual vs automated penetration testing.

Key takeaways

  • DOMPurify versions 3.4.4 through 3.4.16 are affected by a critical bypass (GHSA-cg2g-xh64-v4q7). Version 3.4.17 is the first patched release.
  • The described case is SAFE_FOR_TEMPLATES: true plus RETURN_DOM, RETURN_DOM_FRAGMENT or IN_PLACE. Search your whole source tree for those names.
  • Run npm ls dompurify to see every copy, including nested ones, then run npm install dompurify@^3.4.17 and commit your lockfile.
  • Use overrides (npm), pnpm.overrides or resolutions (Yarn) if a dependency keeps an old copy alive.
  • Add a Content Security Policy and avoid feeding user content to template engines, so one library bug does not decide your safety.

Not sure whether this touches your site?

If you want a quick outside look first, try the free website security check. If you want a person to test how your live site handles user input from the outside, our $49 Circuit audit does that: a human security engineer hand-tests your site and writes up every issue with evidence and exact fixes. An outside audit cannot read your npm dependencies or sanitizer settings, so the npm ls dompurify and grep steps above are still how you check this one. Circuit is a focused audit for small sites, not an enterprise pentest or a compliance certification. See the pricing page for what is included.

Want certainty, not guesswork?

A real human security engineer audits your whole site by hand and sends a full report: every issue, its severity, and the exact fix. From $49, with a 14-day money-back guarantee.

See pricing

Frequently asked questions

Is there a DOMPurify vulnerability I need to patch right now?
Yes, if you run dompurify from 3.4.4 up to 3.4.16. GitHub advisory GHSA-cg2g-xh64-v4q7 rates it critical, and 3.4.17 is the first patched version. Update with `npm install dompurify@^3.4.17` and confirm with `npm ls dompurify`.
What is the DOMPurify template bypass?
When `SAFE_FOR_TEMPLATES` is on together with a DOM output mode (`RETURN_DOM`, `RETURN_DOM_FRAGMENT` or `IN_PLACE`), template expressions like `{{...}}` can survive inside the text of an attached shadow root. The cleanup pass checks `<template>` content but did not check shadow roots. Version 3.4.17 fixes this.
How do I know if my site uses the vulnerable DOMPurify settings?
Search your code for `SAFE_FOR_TEMPLATES`, `RETURN_DOM` and `IN_PLACE`. If you find `SAFE_FOR_TEMPLATES: true` together with a DOM mode and your installed version is between 3.4.4 and 3.4.16, you match the advisory. Even if you do not, updating is a safe and cheap step.
Is the XSS sanitization fix just updating the package?
For this advisory, yes. Update dompurify to 3.4.17 or newer, rebuild, test and deploy. Also commit your lockfile, and use an overrides or resolutions entry if another package pulls in an older copy.
Does updating DOMPurify make my site fully secure against XSS?
No. It closes this specific bug, but no single library makes a site unhackable. Keep dependencies current, escape output, avoid evaluating user content in templates, and add a Content Security Policy as a second layer.

Keep reading

See what attackers see, for free

Run the free passive check on your website. No login, no impact on your site, results in seconds.

Ready for the full manual audit? See pricing

Published by Bug Circuit. Written with AI assistance and reviewed for accuracy before publishing.