XSS in React
What You Will Learn
- How React renders HTML and why it is mostly safe by default
- How React components can still be vulnerable to XSS
- The common patterns that introduce XSS in React apps
- How to avoid XSS when building React applications
What Is It?
React is designed to prevent XSS by escaping output by default. But developers can bypass these protections — accidentally or intentionally — and introduce XSS vulnerabilities.
Understanding React’s internal rendering model helps you find XSS in React applications.
React Components
React components act like functions: they take props as input and return React elements.
React elements are created by components using the createElement() function, which takes three arguments:
React.createElement(
type, // HTML tag or component
[props], // attributes
[...children] // child nodes
)
For example, this JSX:
class HelloWorld extends React.Component {
render() {
return <p title="About">Hello, {decodeURIComponent(document.location)}</p>
}
}
Becomes:
class HelloWorld extends React.Component {
render() {
return React.createElement(
'p',
{title: 'About'},
["Hello, ", decodeURIComponent(document.location)]
)
}
}
Where:
type= the tag name ('p')props= list of attributes{title: 'About'}children= child node content
Ways to Achieve XSS in React
1. Injecting into Props
If attacker-controlled input ends up in the props of a component, it can lead to XSS.
render() {
attackerProps = JSON.parse(attackerInput);
return <div {...attackerProps} />;
}
If attackerInput is {"dangerouslySetInnerHTML": {"__html": "<img src=x onerror=alert(1)>"}}, the div renders with XSS.
2. dangerouslySetInnerHTML
The dangerouslySetInnerHTML prop renders raw HTML directly. It is React’s explicit escape hatch — if you use it with attacker-controlled input, you have XSS.
<div dangerouslySetInnerHTML={attackerInput} />
Never use dangerouslySetInnerHTML with unsanitized user input.
3. Attacker Input in href or formaction
React does not sanitize javascript: URLs in href attributes.
<a href={attackerInput}>Click me</a>
If attackerInput is javascript:alert(1), clicking the link executes JavaScript.
4. As a Function Argument
fn = new Function("attackerInput");
fn();
new Function() is equivalent to eval(). Never pass user input to it.
5. eval()
eval(attackerInput);
Classic — never use eval() with user data.
Controlling type and children
If you can control the type argument (the HTML tag) and the children of createElement(), you may be able to inject XSS even without using dangerouslySetInnerHTML:
React.createElement(attackerControlledType, {}, 'text');
// If attackerControlledType = "script", this renders a <script> tag
Prevention
- Never use
dangerouslySetInnerHTMLwith user input — if you must, sanitize withDOMPurify - Validate URLs before using them in
href— reject any URL starting withjavascript: - Never use
eval(),new Function(), orsetTimeout(string, ...)with user data - Use Content Security Policy (CSP) headers as a defense-in-depth measure
- Treat
__htmlin any prop as dangerous
// Safe URL check
function isValidUrl(url) {
return url.startsWith('https://') || url.startsWith('http://');
}