# 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.

- **Published:** 2025-06-04
- **Author:** Clixo
- **Reading time:** 5 min read
- **Tags:** xss, react, nextjs, web-security, frontend-security
- **Canonical URL:** https://clixo.sh/blog/how-to-prevent-xss-in-react-and-nextjs

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.

```mermaid
flowchart LR
  A["External or user data"] --> B["dangerouslySetInnerHTML"]
  A --> C["javascript: URL in href"]
  A --> D["eval / new Function"]
  A --> E["SSR script tag injection"]
  B --> F["DOMPurify at render point"]
  C --> G["URL scheme validation"]
  D --> H["Remove eval patterns"]
  E --> I["HTML-encode serialized JSON"]
```

## 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.

```js
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.

```js
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 `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.

Building a React or Next.js application that needs to handle user-generated content securely at scale? [Start a build](https://clixo.sh/#contact).

---

Clixo · 1141 W Bryn Mawr Ave, Itasca, IL 60143, US · [hello@clixo.sh](mailto:hello@clixo.sh)
[Start a build](https://clixo.sh/#contact) · [All services](https://clixo.sh/services) · [Agent guide (llms.txt)](https://clixo.sh/llms.txt)
