Cross-Site Scripting lets an attacker's JavaScript run inside your page under your site's identity. React's automatic JSX text escaping neutralizes most of this by default ā but not all of it. This lesson covers exactly where that protection applies and where it doesn't.
1What Cross-Site Scripting Actually Does
Cross-Site Scripting happens when an attacker gets their own JavaScript to execute inside a page, in a victim's browser, under that site's identity ā able to steal cookies, submit forms as the victim, or read page content. It remains one of the most common and dangerous web vulnerabilities.
2React Escapes Text by Default
JSX expressions rendered as text content are automatically HTML-escaped by React. A string containing <script> tags renders as literal, visible text rather than an executable script tag, neutralizing the most common form of injected script without any special handling.
3Where React's Protection Doesn't Reach
React's automatic escaping covers {expression} text content specifically ā it does not protect dangerouslySetInnerHTML, a href or src attribute set to a javascript: URL, or data passed unsanitized into a third-party library that renders raw HTML. Knowing these exact boundaries is essential to staying safe.
4The javascript: URL Attack
A link's href set to a user-supplied string can contain a javascript: protocol value instead of a real URL, executing arbitrary code when clicked. Always validate that user-supplied URLs start with an expected protocol before rendering them into href or src attributes.
5Step-by-Step Breakdown
What Cross-Site Scripting Actually Does. Cross-Site Scripting (XSS) happens when an attacker gets their own JavaScript to run inside your page, in your user's browser, under your site's identity ā able to steal cookies, submit forms as the victim, or read anything on the page. It's one of the most common and dangerous web vulnerabilities.
React Escapes Text by Default. This is the single most important security fact about React: {expression} in JSX is automatically HTML-escaped. If a value contains <script>, React renders it as the literal text <script> on the page ā not as a real script tag ā completely neutralizing the attack without you doing anything special.
If a comment string containing <script>alert('hacked')</script> is rendered with <p>{comment}</p>, what happens?
- āIt's escaped and displayed as literal, harmless text ā the script never executes
- āThe script tag actually executes, triggering the alert
Where React's Protection Doesn't Reach. React's automatic escaping applies to {expression} text content ā it does NOT protect dangerouslySetInnerHTML, a href/src attribute set to a javascript: URL, or data passed unsanitized into a third-party library that renders raw HTML. Knowing exactly where the safety net ends is what keeps you safe.
The javascript: URL Attack. A link's href set to a user-supplied string can contain javascript:alert(document.cookie) instead of a real URL ā clicking it executes that code. Always validate that user-supplied URLs start with an expected protocol (https://, mailto:) before rendering them into href or src.
Why is <a href={userSuppliedUrl}> a potential XSS risk, even though {userSuppliedUrl} looks like it's inside a protected JSX expression?
- āReact's text escaping doesn't prevent a malicious javascript: protocol value from being executed on click
- āThis is actually a JSX syntax error that would fail to compile
Mastery Achieved. You now understand XSS in React: what the attack actually accomplishes, why React's automatic text escaping neutralizes most of it by default, and the specific gaps ā dangerouslySetInnerHTML and unvalidated URLs ā where that protection doesn't reach. Next, you'll go deeper into safe rendering practices generally.
Level Up š
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
XSS protection depends on application code, not browser-specific behavior.
Same protections and risks apply.
Same protections and risks apply.
Same protections and risks apply.
Accessibility (A11y)
1Security and Accessibility Reviews Often Touch the Same Risky Patterns
Components using dangerouslySetInnerHTML or custom URL handling deserve both a security review and an accessibility review, since unsanitized or malformed injected content can also break screen reader parsing.
SEO Implications
- 1
A Successful XSS Attack Can Lead to Search Engine Blocklisting
If an attacker uses an XSS vulnerability to inject malicious content or redirects, search engines can flag and blocklist the affected domain, causing severe and lasting SEO damage beyond the immediate security incident.
Best Practices
Default to Plain JSX Expressions for Any User-Supplied Text
Rendering user content with {content} inside JSX gets automatic escaping for free ā never manually construct HTML strings from user input for text display.
Validate URL Protocols Before Rendering User-Supplied Links
Check that a user-supplied URL starts with an expected protocol (https://, mailto:) and reject or sanitize anything else, including javascript: and data: URLs, before using it in href or src.
Frequent Bugs
A user-supplied profile link executes arbitrary JavaScript when clicked instead of navigating anywhere.
The href value wasn't validated and contained a javascript: protocol payload. Validate that user-supplied URLs match an expected protocol allowlist before rendering them into href.
Developers assume rendering data with {value} inside JSX is unsafe and manually escape it themselves, adding unnecessary complexity.
This is unnecessary ā React already automatically escapes JSX expression text content. Manual escaping on top of React's built-in behavior is redundant and can sometimes cause double-escaping display bugs.
Real-World Examples
Validating a User-Supplied Website Link on a Profile Page
A user profile page lets users add a personal website link, rendered as a clickable <a> tag. Without validation, a malicious user could set their 'website' field to a javascript: URL, which would execute when another user clicked their profile link. Validating the URL's protocol against an allowlist before rendering it into href closes this gap.
function isSafeUrl(url) {
try {
const parsed = new URL(url);
return ['http:', 'https:', 'mailto:'].includes(parsed.protocol);
} catch {
return false;
}
}
<a href={isSafeUrl(user.website) ? user.website : '#'}>{user.website}</a>