How to Prevent XSS in React and Next.js Applications
A practical guide to preventing cross-site scripting (XSS) in React and Next.js apps, covering common pitfalls, escape hatches, and CSP setup.
React's default rendering model encodes output, which prevents most straightforward XSS attacks. This leads many teams to assume they are covered. They are not — the framework has several well-known escape hatches, and the ecosystem around React adds multiple injection surfaces that React itself cannot protect.
This guide covers the specific scenarios where XSS becomes possible in React and Next.js, and the concrete steps to close each gap.
How XSS Works and Why React Does Not Fully Prevent It
Cross-site scripting (XSS) occurs when an attacker injects executable JavaScript into a page that other users subsequently load. The script runs with the same privileges as the legitimate application — it can read cookies, steal session tokens, make authenticated API requests on behalf of the user, or exfiltrate form data.
React prevents XSS in the typical rendering path by encoding string values before inserting them into the DOM. When you write {userInput} inside JSX, React treats it as text, not markup. But this protection does not extend to every rendering path.
Preventing XSS in React and Next.js: The Specific Risks
dangerouslySetInnerHTML
This prop exists for legitimate use cases — rendering sanitized markdown, for example. The problem is that it bypasses React's encoding entirely. Whatever HTML string you pass is inserted directly into the DOM.
If you must use dangerouslySetInnerHTML, sanitize the content with a library like DOMPurify before passing it in. The sanitization must happen immediately before the value reaches the prop — not earlier in the data pipeline, where subsequent transformations could reintroduce malicious content.
import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize(rawHtmlFromUser);
return <div dangerouslySetInnerHTML={{ __html: clean }} />;Do not sanitize in an API handler and then trust the result through multiple transformation steps. Sanitize at the point of rendering.
javascript: and data: URLs
React does not validate the scheme of href or src attribute values. An anchor tag with href="javascript:alert(1)" will execute JavaScript when clicked. React 16.9 added a warning for javascript: URLs but does not block them.
Validate URL values before passing them to href, src, or action attributes. A simple check — ensuring the URL begins with https://, http://, /, or # — blocks the most common vector.
function isSafeUrl(url) {
return /^(https?:\/\/|\/|#)/.test(url);
}eval and Function constructors
Any use of eval(), new Function(), setTimeout with a string argument, or setInterval with a string argument creates a code execution path from data. If any part of that string originates from user input or an external API, you have an injection vulnerability. Avoid these patterns entirely.
Server-Side Rendering in Next.js
Next.js renders HTML on the server and sends it to the client. If you inject unsanitized user data into the rendered HTML — either through __NEXT_DATA__, dangerouslySetInnerHTML, or manual string interpolation in server components — that content is served as part of the initial HTML response.
Treat all data flowing through getServerSideProps, getStaticProps, and server components with the same skepticism you would apply to client-side rendering. Sanitize before rendering, regardless of where rendering happens.
Serializing State into HTML
A common Next.js pattern is embedding server-fetched data into the HTML payload via a script tag so the client can hydrate without an additional fetch. If user-controlled content ends up in that serialization path, an attacker can break out of the JSON string context and inject a script.
Use a serialization library that handles HTML encoding of special characters, or JSON-encode and then HTML-encode the output. Never use JSON.stringify and embed the result directly into a script tag without additional escaping.
Third-Party Scripts
Analytics, A/B testing, chat widgets, and ad scripts run with full page privileges. A compromised or malicious third-party script is XSS by another name. Limit third-party scripts to those that are strictly necessary, load them from trusted hosts you pin in your Content Security Policy, and review them periodically.
Content Security Policy as a Defense-in-Depth Layer
A properly configured Content Security Policy (CSP) does not prevent XSS, but it limits what injected scripts can do. A CSP that restricts script-src to specific trusted origins means that even if an attacker injects a script tag pointing to an external host, the browser refuses to load it.
In Next.js, you can set CSP headers in next.config.js using the headers() function, or in a middleware file. Start with a report-only policy to understand what would be blocked before switching to enforcement mode.
Key Practices Summarized
- Never use
dangerouslySetInnerHTMLwithout running DOMPurify immediately before - Validate URL schemes before using user-supplied values in href, src, or action
- Eliminate
eval()and string-based timer functions from your codebase - Sanitize server-side rendering data the same way you sanitize client-side data
- Escape special characters when embedding JSON in script tags
- Audit third-party scripts and enforce a CSP that restricts their sources
- Enable React's strict mode, which surfaces deprecated patterns that increase XSS risk
What Automated Tools Can and Cannot Do
Static analysis tools like ESLint with eslint-plugin-security will flag dangerouslySetInnerHTML usage and eval() calls. DAST tools can detect reflected XSS in HTTP responses. Neither will reliably catch XSS that depends on complex data flows or third-party integrations. Manual review of rendering paths that handle external or user-supplied data remains necessary.
Building a React or Next.js application that needs to handle user-generated content securely at scale? Start a build.