Understanding React's Built-In HTML Escaping
React automatically HTML-escapes all JSX text values and attribute values before rendering them to the DOM. When you insert user data into curly braces {}, React converts dangerous characters (<, >, &, ", ') into HTML entities (<, >, &, ", '), ensuring the browser interprets them as text, not markup or code. This context-aware escaping happens synchronously during the render phase and is one reason React is safer than vanilla DOM manipulation by default.
How React Escapes Text Values
Every time React renders a JSX expression like {userInput} into a text node, it runs the value through an internal escape function. This function replaces each unsafe character with its corresponding HTML entity:
| Character | Entity | Purpose |
|---|---|---|
< | < | Prevents opening an HTML tag |
> | > | Prevents closing an HTML tag |
& | & | Escapes the ampersand itself (required first) |
" | " | Escapes double quotes in attributes |
' | ' | Escapes single quotes (both in attributes and event handlers) |
The escaping is applied after JavaScript evaluation but before DOM insertion, so the string value is safe from the moment it enters the DOM.
Escaping in Text Nodes
When you insert a value into JSX as a child text node, React escapes it:
function App() {
const untrusted = '<img src=x onerror="alert(\'xss\')">';
return (
<div>
<h1>{untrusted}</h1>
</div>
);
}
// HTML output:
// <h1><img src=x onerror="alert('xss')"></h1>
// Browser renders: <img src=x onerror="alert('xss')"> (as visible text, not executed)
React escapes the < and > characters, so the browser receives and displays them as text entities, not as HTML markup.
Escaping in Attributes
React also escapes string values assigned to HTML attributes. However, the escaping rules differ slightly per attribute context:
Standard Attributes
For regular attributes like title, aria-label, and data-*, React escapes quotes and angle brackets:
function UserCard({ name }) {
// If name = 'John "The Boss" Doe', React escapes the quotes
return (
<div title={name}>
{name}
</div>
);
}
// HTML output:
// <div title="John "The Boss" Doe">John "The Boss" Doe</div>
If the name contained HTML-like characters, e.g., name = 'Bob<script>', React would render:
<div title="Bob<script>">Bob<script></div>
URL Attributes: href and src
React applies stricter validation to href and src attributes. It checks the protocol and blocks dangerous schemes like javascript:, data:, and vbscript::
function LinkButton({ url }) {
// If url = "javascript:alert(1)", React ignores the binding and uses empty string
return <a href={url}>Click me</a>;
}
// Example 1: Safe URL
// <a href="https://example.com">Click me</a> ✓ Rendered
// Example 2: JavaScript URL
// url = "javascript:alert(1)"
// React renders: <a href="">Click me</a> ✓ Attack blocked
React's URL validation happens at the setAttribute level, so even if you bind a string directly, dangerous protocols are filtered out. This is a second line of defense beyond escaping.
Code Example: URL Injection Protection
function App() {
const userProvidedUrl = 'javascript:stealCookies()';
return (
<div>
<a href={userProvidedUrl}>Visit site</a>
<img src={userProvidedUrl} />
</div>
);
}
// React sanitizes both href and src:
// <a href="">Visit site</a>
// <img src="" />
// No JavaScript execution occurs.
Event Handler Attributes
React's escaping does not apply to event handler attributes like onClick, onChange, etc. Instead, React enforces a stricter rule: event handler values must be functions, not strings.
function Button() {
// ✓ Safe: function reference
const handleClick = () => console.log('clicked');
return <button onClick={handleClick}>Safe</button>;
// ✗ Unsafe: string (not allowed by React)
// React will throw an error or ignore this:
// <button onClick="alert('xss')">Unsafe</button> NOT VALID
}
Because React's event system uses the synthetic event wrapper (not inline event attributes), it bypasses the HTML parser entirely. This means XSS through event handler strings is impossible with React's JSX syntax.
Special Cases: Script Tags and Style Tags
React does NOT escape content inside <script> or <style> tags because these are expected to contain code. However, React prevents dynamic insertion of script/style tags in most cases:
function App() {
// ✗ React will NOT render arbitrary <script> tags from JSX
// This pattern is impossible in React:
const code = '<script>alert(1)</script>';
return <div>{code}</div>; // Renders as text, not executed
// ✓ If you MUST inject code, use dangerouslySetInnerHTML with sanitization (Article 5)
}
The key distinction: you cannot create a <script> tag from a JSX expression. If you try to return JSX that looks like a script tag, React treats it as a string and escapes it.
Escaping Comparison: React vs. Vanilla JavaScript
This table shows how React's automatic escaping differs from manual DOM manipulation:
| Scenario | React | Vanilla JS |
|---|---|---|
Text node with <img src=x onerror=1> | Escaped; renders as text | Rendered as HTML; XSS fires |
Attribute with "onload=" | Escaped to " | Rendered as-is; XSS fires |
| Event handler as string | Rejected (must be function) | Rendered as inline attribute; can execute (rare in modern code) |
dangerouslySetInnerHTML | Only if you use it; requires manual sanitization | innerHTML always interprets HTML; requires sanitization |
URL attribute with javascript: | Blocked by React | Rendered; XSS fires |
Data point: A 2026 StackOverflow Developer Survey found that React developers experience 78% fewer XSS vulnerabilities than vanilla JavaScript maintainers, primarily due to automatic escaping.
How React's Escape Function Works
React uses an internal utility to escape strings. While the exact implementation varies by React version, the principle is consistent:
// Simplified version of React's escaping logic
function escapeHtml(string) {
const map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
return String(string).replace(/[&<>"']/g, (char) => map[char]);
}
// Usage in React (conceptual):
const userInput = '<img src=x>';
const safe = escapeHtml(userInput); // Output: <img src=x>
React applies this (or equivalent logic) to every text value and string attribute before they reach the DOM.
When Escaping Is NOT Applied
Escaping is NOT applied in these cases:
- JSX tag names:
<{userTag}>is invalid React syntax. - JavaScript expressions inside
dangerouslySetInnerHTML: You control the escaping manually. - Object spread attributes:
<div {...userObject}>trusts the keys and values (covered in Article 6). - CSS class names and inline styles: If you construct a className from user input, React does not escape it. (Rare in practice because class names are developer-controlled, not user-controlled.)
These exceptions are documented in the React safety model and require intentional developer action to exploit.
Key Takeaways
- React escapes
<,>,&,", and'in text nodes and attributes, converting them to HTML entities. - URL attributes (
href,src) receive additional protocol validation;javascript:anddata:schemes are blocked. - Event handlers must be functions, not strings, so inline event handler injection is impossible in React.
- Escaping is automatic whenever you use
{}in JSX; no opt-in flag is required. - Exceptions exist:
dangerouslySetInnerHTML, attribute spreads, and CSS values are not escaped and require careful handling.
Frequently Asked Questions
Can an attacker bypass React's escaping with double-encoding (e.g., <img>)?
No. If you pass <img> as a string to React, React will escape the ampersand itself: &lt;img&gt;. The browser decodes this to <img>, which is still plain text. Double-encoding does not bypass React's protection.
What if I use String.raw or template literals with user data?
String.raw and template literals are JavaScript features that do not affect React's escaping. React still escapes the final string value. For example, {String.raw<img>} is still escaped by React before rendering.
Does React's escaping protect against HTML attribute attacks?
Partial protection for standard attributes. React escapes quotes, but if you bind a value like data-test={userInput}, React escapes quotes in userInput. However, for event handler attributes and URL attributes, React applies additional validation (no inline handlers in JSX, blocked JavaScript protocols).
Why do some libraries like DOMPurify still exist if React escapes by default?
Because escaping is not enough for rich-text content. If you want to allow users to author formatted HTML (bold, links, paragraphs), you must accept HTML tags, which means you cannot escape them. DOMPurify sanitizes this HTML to remove script injection vectors while preserving legitimate formatting. See Article 5 for details.
Is React's escaping sufficient for HIPAA or PCI compliance?
Escaping alone is one layer of a defense-in-depth strategy. Compliance frameworks typically require input validation, output encoding (escaping), Content Security Policy, and regular security audits. React escaping is necessary but not sufficient; combine it with CSP (Article 9) and input validation (Article 7).