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: trueRETURN_DOM_FRAGMENT: trueIN_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 situation | What to do |
|---|---|
dompurify 3.4.4 to 3.4.16, with SAFE_FOR_TEMPLATES: true and RETURN_DOM, RETURN_DOM_FRAGMENT or IN_PLACE | This 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 output | The 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 set | The 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 newer | Patched for this advisory. |
| Versions older than 3.4.4 | Not 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.
- 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
- Confirm what you now have:
npm ls dompurify
- If another package pulls in its own older copy, force a safe version in
package.jsonusing npm'soverridesfield, then reinstall:
"overrides": {
"dompurify": "^3.4.17"
}
- Rebuild, run your tests, and deploy. Open a page that shows user-supplied content and make sure it still looks right.
- Commit the updated lockfile (
package-lock.json,pnpm-lock.yamloryarn.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: truewithRETURN_DOM,RETURN_DOM_FRAGMENTorIN_PLACEuntil 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 auditflags 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: trueplusRETURN_DOM,RETURN_DOM_FRAGMENTorIN_PLACE. Search your whole source tree for those names. - Run
npm ls dompurifyto see every copy, including nested ones, then runnpm install dompurify@^3.4.17and commit your lockfile. - Use
overrides(npm),pnpm.overridesorresolutions(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.
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