WritingHow to Prevent XSS in React and Next.js Applications — LinkIT
5 min readxss, react, nextjs, web-security, frontend-security

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 encodes string values in its normal rendering path, but raw HTML, external URLs, custom serialization, and third-party scripts need separate review. Trace untrusted data to each rendering context and apply controls appropriate to that context.

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 19 blocks javascript: URLs in href and src; older releases handled these differently. Validate untrusted URLs against the schemes and destinations your application permits, including cases outside React's normal rendering path.

Validate URL values before passing them to href, src, or action attributes. The example below is a simplified starting point, not a complete destination policy. Parse and normalize URLs, and reject unwanted external origins; a leading / also matches protocol-relative URLs such as //example.com.

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, using escaping appropriate to an HTML script context. 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 dangerouslySetInnerHTML without 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.