Run untrusted HTML through a maintained sanitizer immediately before it reaches the DOM, allow only the markup you need, and do not change the result afterwards. In React that usually means sanitizing and then passing the clean string to dangerouslySetInnerHTML. Keep the sanitizer updated.
When you need this
Most pages should render user text as plain text, which needs no sanitizer. You need one when the product genuinely shows user or third party HTML: rich text comments, rendered markdown, CMS content, email previews. In those cases OWASP recommends an allowlist sanitizer and says to enforce Trusted Types with no permissive default policy.
A routine that holds up
- Pick a maintained sanitizer. DOMPurify is a widely used example. Its README describes it as a DOM-only XSS sanitizer for HTML, MathML and SVG.
- Sanitize as late as possible, in the component or a small helper right before rendering, so the sanitized string cannot be altered along the way.
- Narrow what it allows. The README notes that by default it permits HTML, SVG and MathML, and shows how to restrict to HTML only with a profile setting.
- Do not edit the output. The README warns that if you first sanitize HTML and then modify it afterwards, you might easily void the effects of sanitization, and that the same applies if you pass the clean markup to another library that changes it.
- Wrap the call in a single helper used everywhere, so a lint rule can ban direct use of dangerouslySetInnerHTML elsewhere.
import DOMPurify from 'dompurify'
export function cleanHtml(dirty) {
return DOMPurify.sanitize(dirty, { USE_PROFILES: { html: true } })
}
Server side rendering needs extra care
If you sanitize on the server, the sanitizer needs a DOM implementation. The DOMPurify README says sanitization can end in XSS even when the sanitizer does everything correctly if the server side DOM has flaws, and it advises keeping that DOM library up to date, citing known attack vectors fixed in a later jsdom version. The DOM library you choose becomes part of your trusted computing base, so put it in your dependency review.
What a sanitizer does not do
| Question | Sanitizer answer |
|---|---|
| Does it validate links you set in JSX yourself? | No, it cleans the markup you pass in, so validate your own URL props separately |
| Does it make the source trustworthy? | No, it reduces risk from markup, not from content |
| Is it set and forget? | No, update it, since attackers keep finding parser tricks |
| Does it replace CSP? | No, use both |
Track the sanitizer like any dependency
Because a sanitizer is a security control, a vulnerable old version is a real exposure. Put it on a short update cycle, watch advisories and check it appears in your inventory and not only in a lockfile of one app.
Try it yourself
Click through it at your own pace, or open it full screen. Look at how packages and versions are listed per project, so you can confirm which apps use which sanitizer version.
Safeguard's dependency scanning and the free scan at gold.safeguard.sh/scan can show the sanitizer version in each project.
Quick check
Can I sanitize once when saving and skip it on render?
Sanitizer rules and attack knowledge change over time, so sanitizing on output, right before use, is the safer habit.
Is DOMPurify required?
No. Any maintained allowlist sanitizer works. The point is to use one, restrict it and avoid editing its output.
Sources
Last reviewed: 19 October 2026.