React offers strong protections against XSS. However, it’s far from perfect, and a variety of workarounds exist to make XSS doable in React XSS. In this article, we’ll show you the major weaknesses in React that open up the chance for XSS.
React developers often neglect proper XSS defense, assuming that React protects you from all XSS issues. As a result, React apps tend to have small XSS vulnerabilities littered about. In other words, React apps are an easy target for bug bounties. Besides, React is also pretty easy to learn. So even hackers without strong coding skills can quickly pick up React.
We’ll start with the easiest vulns to find, and slowly work to the more advanced ones.
Using dangerouslySetInnerHtml
Imagine you have a string, userInput, and you want to render it inside of a React component. You might do something like this:
const RandomComponent = (_props) => {
const userInput = '<b>this text is bold</b>';
return (
<div>{userInput}</div>
);
}
What do you think will happen when you render this component? If you’re experienced with React, you probably know that the HTML content inside of userInput will not render. React escapes such content. But sometimes, you want to load HTML content directly from a string. What then?
In such cases, React offers the dangerouslySetInnerHTML attribute. It works like this:
const RandomComponent = (_props) => {
const userInput = '<b>this text is bold</b>';
return (
<div dangerouslySetInnerHTML={{"_html": userInput}} />
);
}
However, this introduces a security risk – what if the HTML contains XSS? We simply render whatever’s in the string! One solution is to use a library like DOMPurify to remove risky tags and attributes, while leaving other tags untouched. We can use it like so:
import DOMPurify from 'dompurify';
const RandomComponent = (_props) => {
const rawUserInput = '<b>this text is bold</b>';
const userInput = DOMPurify.sanitize(rawUserInput);
return (
<div dangerouslySetInnerHTML={{"_html": userInput}} />
);
}
Links with javascript: URLs
Often, websites with some social functionality will allow users to create profiles that include links (eg, linking to your Twitter account, personal blog, etc). However, this introduces the risk of someone supplying a javascript: URL. If you render a link like this:
<a href="javascript:alert(1)">click me</a>
The JavaScript code will execute in the user’s browser when they click the link.
React doesn’t protect from this by default. However, there exist multiple ways for you, as a developer, to protect against this. For example, you can:
- Use DOMPurify on the HTML code (if it was originally a string).
- Check for javascript: URLs with a regex.
- Only allow https:// (and maybe http://) protocols.
- Deploy a Content Security Policy to block any code executed from javascript: URLs.
Markdown XSS
Markdown XSS is really simple. Although not technically unique to React apps, I find it most commonly in React apps.
Technically, markdown allows arbitrary HTML. You can simply put an <img> tag with the onerror attribute in markdown, and viola, XSS!
For an example of this kind of primitive markdown XSS in the wild, see this bug bounty walkthrough from CodeCast.io.
Savvier platforms will prevent this. In that case, you can use markdown’s built-in link format to create a javascript: link, like so:
[click here](javascript:alert('HAXXED'))
Preventing this depends on the specific markdown library the app uses. However, since it’s such a common concern, almost every markdown library offers some way to address this.
Direct DOM access
React makes it possible to dynamically create and modify DOM elements, with createRef and findDomNode respectively. So instead of creating a “proper” JSX element, you can do something like this:
const RiskySpan = () => {
const spanRef = createRef();
useEffect(()=>{
spanRef.current.innerHTML = "<img src='' onerror='alert(1)'></img>";
},[]);
return (
<span className="container" ref={spanRef} />
);
}
When done this way, the contents of innerHTML will render as actual HTML code in the browser with no sanitization from React.
The exact same issue can pop up when we directly access a node on the DOM using findDomNode, and then modify its contents with innerHTML.
Luckily, you can fix this by simply setting innerText instead of innerHTML. But what if you have no choice, and the situation forces you to use innerHTML? For example, let’s imagine you are embedding an iframe from Youtube or some other platform, and you don’t know what HTML blob it will give you. In this case, you can use the same library we used to mitigate the risks of dangerouslySetInnerHTML. That is, DOMPurify.
The sanitized code would look like this:
spanRef.current.innerHTML = dompurify.sanitize("<img src='' onerror='alert(1)'></img>");
Pretty easy! But also easy to forget to do, which is why it’s best to avoid directly setting the inner HTML code of dynamic elements in this way.
React XSS from Injecting props
Some apps will accept JSON from (for example) a POST request, and then convert that directly into props for a JSX component. The pattern look like this:
const RiskyProps = () => {
const userSuppliedProps = getPropsFromUserPOST();
return (
<div {...userSuppliedProps} />
);
}
Sadly, this pattern is quite common in web applications. However, that’s good news for us as hackers and tinkerers! So what’s the problem? Well, a bad actor could supply a malicious value for userSuppliedProps like so:
{
"dangerouslySetInnerHTML": {"_html": "<img src='' onerror='alert(1)'></img>"}
}
And with that, our friend from before (dangerouslySetInnerHTML) strikes again! Be careful not to load arbitrary props into a component, especially if there’s any chance those props came from a user. Instead, deconstruct the object to only access props you know you want, and make sure the values make sense.
For example, we could fix the antipattern above by simply deconstructing getPropsFromUserPOST with whatever values we want to turn into attributes in our div element. The fixed, correct pattern looks something like this:
const RiskyProps = () => {
const { name, age } = getPropsFromUserPOST();
return (
<div name={name} age={age} />
);
}
If we refactor the code this way, malicious users have no way to create an unwanted dangerouslySetInnerHTML attribute to launch an XSS exploit.
Learn more about React XSS exploits
There are infinite possible exploits that could effect a React app. The best way is to simply experiment with React, stay up to date, and aim to understand every layer of the web stack. Hopefully, this guide has given you a starting point to begin finding bugs and patching vulnerabilities.
For further reading to continue your learning as a React XSS expert, I highly recommend that you consider checking out some of the following sources:
- What does it mean when they say React is XSS protected?
- HackerOne writeups for XSS bug bounties in React apps.
- Preventing XSS in React by Pragmatic Web Security
- Things you forgot (or never knew) because of React
- Posts on Hacker News about React XSS
Don’t fall into the trap of thinking React makes XSS obsolete. As more bug bounty hunters make this mistake and ignore React apps, we have an opportunity to find easy bugs.
Never stop learning, and happy hacking!
Leave a Reply