Prevent cross-site scripting (XSS) in a Next.js + React app — how TSX auto-escapes values, the danger of dangerouslySetInnerHTML and how to sanitize, and blocking unsafe URLs.
Why: XSS is when an attacker gets their JavaScript to run in your users' browsers — usually by smuggling a <script> or an event handler into content your app renders. The good news: React/TSX escapes any value you interpolate by default, so {userInput} renders as text, not HTML. Most XSS in a React app comes from the few places you opt out of that.
// Safe by default — React escapes the value, so this renders as TEXT:
function Comment({ text }: { text: string }) {
return <p>{text}</p>
}
// Even if text is: <img src=x onerror="alert(document.cookie)" />
// the browser shows the literal characters — no script runs.Note: the one common way to reintroduce XSS is dangerouslySetInnerHTML — it injects a raw HTML string with no escaping. The name is a warning. Never pass user-controlled HTML to it directly; if you must render HTML (a CMS field, rendered markdown), sanitize it first with a vetted library.
import DOMPurify from 'isomorphic-dompurify'
// DANGEROUS — if `html` contains a <script> or onerror, it runs:
// <div dangerouslySetInnerHTML={{ __html: html }} />
// SAFE — sanitize the HTML first, then inject the clean result:
function RichText({ html }: { html: string }) {
const clean = DOMPurify.sanitize(html)
return <div dangerouslySetInnerHTML={{ __html: clean }} />
}Note: escaping does not cover href and src attributes. A value like javascript:alert(1) in an <a href> runs when the link is clicked. Validate that any user-provided URL uses a safe scheme (http, https, mailto) before putting it in an attribute.
function isSafeUrl(url: string) {
try {
const { protocol } = new URL(url, window.location.origin)
return ['http:', 'https:', 'mailto:'].includes(protocol)
} catch {
return false // not a valid URL at all
}
}
function ProfileLink({ url }: { url: string }) {
const href = isSafeUrl(url) ? url : '#'
return <a href={href}>Visit</a>
}