Common React vulnerabilities, how to patch them, and how to spot them in a code review
React is my favorite library for making interactive interfaces. It is both easy to use and quite secure! However, That doesn’t mean it’s completely safe. It’s easy to get complacent and think “we don’t have to worry about XSS because we use React!”
React vulnerabilities most often happen when you think you’re using the library but aren’t. It’s important to know what React does and doesn’t handle for you.
The following are the most common specific mistakes teams make, how to spot them, and how to fix them.
First, a (very) quick primer on Cross-Site Scripting
Cross-site scripting (XSS) is a potentially serious client-side vulnerability. It happens whenever an attacker can trick a website into running JavaScript in their target’s browser.
Reflected cross-site scripting is where a link contains information that gets filled into the page and then read as code in the browser – for example, forms being filled in based on a user-controlled query string.
Stored cross-site scripting is where something uploaded by an attacker gets rendered in a page. Comments and image uploads are common attack vectors for stored XSS.
The Samy Worm exploited an XSS vulnerability in MySpace and was the fastest spreading virus of all time. Vulnerable websites can put users at risk of having login credentials or personal data stolen, and it’s a common way to exploit other vulnerabilities. Malicious scripts are most often used to spam and redirect users to fraudulent sites which can damage a site’s reputation and/or SEO.
All of the following pitfalls put an application at risk of XSS.
Pitfall #1: Server-side rendering attacker-controlled initial state
Sometimes when we render initial state, we dangerously generate a document variable from a JSON string. Vulnerable code looks like this:
<script>window.__STATE__ = ${JSON.stringify({ data })}</script>
This is risky because JSON.stringify()
will blindly turn any data you give it into a string (so long as it is valid JSON) which will be rendered in the page. If { data }
has fields that un-trusted users can edit like usernames or bios, they can inject something like this:
{ username: "pwned", bio: "</script><script>alert('XSS Vulnerability!')</script>" }
This pattern is common when server-side rendering React apps with Redux. It used to be in the official Redux documentation, so many tutorials and example boilerplate apps you find on GitHub still have it.
Don’t believe me? Try it yourself. Google “react server-side rendering example app” and try this attack on any of the top results.
How to spot it in a code review
Look for JSON.stringify()
being called with a variable that may have un-trusted data inside a script tag:
Here is the example that used to be in the Redux docs:
And here’s an instance from an example app I found on GitHub:
Sometimes this vulnerability is a little harder to spot. The following example would also be vulnerable if context.data
is not properly escaped.
When server-side-rendering is happening, look at what is being rendered. If user input is not properly escaped and is rendered in the DOM, it could be dangerous.
The Fix
One option is to use the serialize-javascript NPM module to escape the rendered JSON. If you are server side rendering with a non-Node backend you’ll have to find one in your language or write your own.
$ npm install --save serialize-javascript
Next, import the library at the top of your file and wrap the formerly vulnerably window
variable like this:
<script>window.__STATE__ = ${ serialize( data, { isJSON: true } ) }</script>
For further reading on this patch, refer to this fantastic article by Emelia Smith.
Pitfall #2: Sneaky Links
An <a>
tag can have an href
that links to another page, an external site, or somewhere on the same page. They can also contain scripts prefixed with javascript: stuff()
. If you didn’t know about this feature of HTML, try it now by copy-pasting this into your browser bar:
data:text/html, <a href="javascript: alert('hello from javascript!')" >click me</a>
What that means for web developers is that if you’re setting links based on user input, an attacker can add a payload prefixed with javascript:. The malicious script will then run in a target’s browser if they click the bad link.
This pitfall is definitely not unique to React, but is one React devs often fall into because they expect the value to be automatically escaped. A future version of React will make this pitfall less likely to accidentally happen.
How to Spot it in a Code Review
Can users add links that other users may click on? If so, try adding a ‘link’ like this:
javascript: alert("You are vulnerable to XSS!")
If the alert pops up on the page, you have an XSS vulnerability. Try everywhere these custom links are loaded. Most likely, not every instance will be vulnerable.
How to Fix It
This fix isn’t unique to React, will vary depending on the application and may be better handled on the backend.
One may think to simply remove the javascript:
prefix. This is an example of blacklisting which isn’t a good strategy for sanitizing data. Hackers have clever ways to bypass filters like this, so instead (or in addition) make sure that links use a whitelisted protocol like https:
and escape HTML entities.
Another strategy that may add additional protection is to make user links open in new tabs. I would recommend against this being the only safeguard, however. Opening a javascript:
link in a new tab is undefined behavior. Most browsers will run the script harmlessly in a blank tab, but it isn’t guaranteed and may be possible to get around depending on the browser.
While you’re at it, add a rel="noopener"
tag to those <a>
s to prevent reverse tabnabbing.
Consider using a special UserLink component so a vulnerable <a>
tag is less likely to be accidentally added in the future. It’s also worth adding a few tests and/or linting rules to make sure code with this mistake doesn’t make it into production.
For further reading on escaping attacker-controlled props, check out this detailed article by Ron Perris.
Links aren’t the only thing that can be exploited in this way but they’re the most likely to be in React. Any element will be vulnerable to this attack if an attacker can control a URI value. Another possibility, for example, is <img onerror="xss()" />
. For the full list of attributes that can contain URIs, CTRL+F for %URI
in this list.
Pitfall #3: Misunderstanding What it Means to Dangerously Set
I greatly appreciate React for putting a security warning right in the name of a method: dangerouslySetInnerHTML
. Despite this warning, we still frequently see developers dangerously setting bad things. Same goes for eval()
.
Take this example that I found on the first page of a google search:
It’s an example of pitfall #1 but with a twist that should have made it extra obvious that something is wrong. To explain themselves, the tutorial states:
We dangerously Set The Inner HTML as a method of sanitizing data and preventing XSS attacks
This is wrong. Don’t do this.
Another example, to prove that this does in fact happen all the time, is this team who found they had a vulnerability where they were adding Markdown to a page using dangerouslySetInnerHTML
. To mitigate this in the future, they added a linting rule.
How to spot it in a code review
It’s good to do a CTRL+F for dangerouslySetInnerHTML
and eval()
before submitting or merging any pull requests (I do this for console logs too) and/or add a linter warning.
Make sure that any instance of this method loads only trusted data into the page. How do you know if the data is trusted? If it doesn’t come from you, it can be tampered with. This includes data loaded from external APIs and stuff that goes through a Markdown library.
For further reading, look at the documentation. Seriously.
Footnote about component spoofing
In 2015, someone figured out that you can spoof components by passing JSON to a component that expects text. I could only find this one instance of component spoofing being reported and the long discussion it sparked about how much React should be responsible for preventing XSS. In the end, the React devs pushed a fix that seems to prevent the vulnerability.
I decided not to include it in this article, but the topic could be an interesting one for further research.
Footnote on SSR
The server-side rendering mistake is so widespread because it was in the Redux documentation and spread from there. They fixed it in 2016, but 3 years later, intro tutorials all over the internet are still teaching it.
So as a challenge to readers, find one example on GitHub and submit a pull request to fix it. Here is an example from our own research.
Together, we can squash this bug once and for all!