React XSS Guide:
Examples and Prevention

stackhawk

StackHawk|July 18, 2021

Understand what XSS (cross-site scripting) is and how you can prevent an XSS attack on your React application.

The web has grown vastly over the years in terms of technologies, frameworks, complexity, and utility. Today, more than a billion people browse through thousands of websites every day. As a result, the internet is always flooded with sensitive data like user credentials, credit card details, etc. 

Therefore, developers must be aware of common vulnerabilities that hackers can exploit to misuse their users' data. One such vulnerability is cross-site scripting (XSS). In this post, you'll understand what XSS is and how it impacts your users. You'll also learn how far React protects your app from XSS attacks and steps to make your React application immune to XSS.

React XSS Guide: Examples and Prevention image

Add Automated Security Testing to Your Pipeline for Free

XSS Attacks: An Overview

JavaScript lies at the heart of the client-side of any application. This is because your application's front end roughly amounts to some JavaScript code running on your browser. 

To illustrate this, let's take a simple example of an online transaction. When you carry out a transaction on a website, it runs some JavaScript to grab your credentials from the input fields and process them. However, the developers can easily run some additional JavaScript to do something detrimental with that information. 

That's precisely what XSS is. An attacker can exploit your application's vulnerability to inject some malicious script into your user's browser, carrying out an XSS attack. Now that you know what an XSS attack is, let's understand how it can happen with an example. 

How Can an XSS Attack Happen?

One of the most common types of XSS attacks is a DOM-based XSS attack. When you mutate DOM directly, it becomes easy for an attacker to inject it with data containing malicious JavaScript. 

Consider the following HTML code. It simply renders some basic markup with an empty <div> element.

<html>
   <body>
       <div id="validation">
       </div>
       <input placeholder="Enter your referral code below" />
       <button>Submit</button>
   </body>
</html>

The above code renders an <input> element on the page with a submit button. On pressing the submit button, you fire a function. Inside the function, you evaluate what the user has entered. You then provide a feedback message to the user based on the result inside the empty <div> element.

const validationElement=document.getElementById('validation');
const validationMessage=`Oops! This seems like an invalid referral code.`
validationElement.append(validationMessage);

Using the append method, you render a message inside your empty <div> element. However, this exposes a vulnerability in your application. Consider the following JavaScript code:

Oops! This seems like an invalid referral code.
<script>
  ...
  alert('Congrats! You've won a prize');
  ...
</script>

The attacker basically renders the validation message along with a malicious <script>. This was possible because the application was modifying DOM directly using the append() method on the <div>. Inside the <script>, the attacker can write code that steals your confidential and sensitive information. On similar grounds, if you use innerHTML to mutate DOM directly, you're exposing your application to a potential XSS attack. 

Thus, an XSS attack can be an alarming sight for your users. However, front-end frameworks have come a long way and provide some protection against such attacks out of the box. Let's look at how React handles these situations for you and how far it secures your application against an XSS attack. 

Is React XSS Foolproof?

Luckily, React does a few things under the hood to safeguard your application against XSS attacks. Let's rewrite the code in the previous section in React as shown:

import './App.css';
import {useState} from 'react';

function App() {
  const [validationMessage,setValidationMessage]=useState('');
  const validateMessage=async()=>{
   setTimeout(()=>{
    setValidationMessage('Invalid referral code')
   },1000)
  }
  return (
    <div className="App">
      <input placeholder="Enter your referral code"/>
      <button onClick={validateMessage}>Submit</button>
      <div>
        {validationMessage}
      </div>
    </div>
  );
}

export default App;

Just like before, I have an <input> element with a <button> that fires the validateMessage function. I have created a state validationMessage that I set inside the validateMessage function using a setTimeout. Finally, I output the validationMessage inside an empty <div> element using JSX.

<div>
{validationMessage}
</div>

React outputs elements and data inside them using auto escaping. It interprets everything inside validationMessage as a string and does not render any additional HTML elements. This means that if validationMessage was somehow infiltrated by an attacker with some <script> tags, React would simply ignore it and render it as a string. 

To demonstrate this, I'll make a slight modification to the validateMessage method as shown below:

  const validateMessage=async()=>{
   setTimeout(()=>{
    setValidationMessage(`Invalid referral code, <script></script>`)
   },1000)
  }

If you check now, the <script> tags get rendered as strings instead of a DOM element.

React XSS Guide: Examples and Prevention image

React JSX auto escape.

Now, any JavaScript enclosed by the <script> tags will not be executed. Thus, the above behavior protects your application from an attacker trying to execute a DOM-based XSS attack. React's official docs also mention this here. Thus, using JSX to conditionally output some content or data to your DOM safeguards it against an XSS attack. 

But does that mean your React application is safe from all kinds of XSS attacks? We only considered the use case of outputting an element or a string using JSX. What if we actually need to render HTML elements directly on the DOM from inside the JSX?

Render HTML Elements Dynamically in React

The most common use case where you'd want to render HTML elements directly is a blogging application. In typical blogging applications, you receive your blogs as a combination of HTML elements. These elements wrap your blog's content, preserving its formatting. 

Let's say you have a small React component that gets a blog from the server and renders it on the DOM. Consider the following code:

import './App.css';

function App() {
  const blog=`
   <h3>This is a blog title </h3>
   <p>This is some blog text. There could be <b>bold</b> elements as well as <i>italic</i> elements here! <p>
   
  `
  return (
    <div className="App">
      <div>
        {blog}
      </div>
    </div>
  );
}

export default App;

Inside the component, I have a blog variable that stores your blog's content wrapped in proper HTML elements. If you directly output the blog variable inside your JSX, it would be interpreted as a string.

React XSS Guide: Examples and Prevention image

Rendering a blog using JSX.

While that safeguards your application against a DOM-based XSS attack, it ruins the experience for your users. Therefore, you need to render your blog as a markup instead of rendering it as a string. This will render your content along with its dedicated HTML tags. 

React allows you to do that using a prop called dangerouslySetInnerHTML. You can pass this prop to any generic container element. It takes in an object with a key _html whose value is the HTML markup you wish to render inside the container.

<div className="App">
<div dangerouslySetInnerHTML={{__html:blog}}>
</div>
</div>

If you check back now, you should see your blog with its intended formatting.

React XSS Guide: Examples and Prevention image

Rendering formatted blog using dangerouslySetInnerHTML

All HTML elements contained by the blog variable are properly rendered on the DOM. However, this puts us back at square one! We again have an XSS vulnerability in our application, and the attacker could inject some malicious scripts inside the blog variable. In fact, the dangerouslySetInnerHTML prop intentionally has the word "dangerous" in it to remind you that you should be cautious while using it. React's official docs also mention this here.

Sanitize Data in React

In order to protect your application from a DOM-based XSS attack, you must sanitize data that contains HTML elements before rendering it on the DOM. There are a number of libraries out there that you can use. One such library is DOMPurify. Let's see how we can use it in our React application.

Let's first install DOMPurify inside our React application by running the following command:

npm i dompurify

To use it, import DOMPurify from the library at the top as shown:

import DOMPurify from 'dompurify';

Let's create a new variable, sanitizedBlog, that contains the sanitized version of our blog.

  const sanitizedBlog=DOMPurify.sanitize(blog)

Finally, we can now use sanitizedBlog instead of blog inside the dangerouslySetInnerHTML prop as shown:

<div className="App">
<div dangerouslySetInnerHTML={{__html: sanitizedBlog}}>
</div>
</div>

Everything should still work the same, but your sanitizedBlog is now protected against any malicious XSS injections. There are other libraries out there that do this, like sanitize-html-react. You can try them out or learn more about how DOMPurify works here.

Escape Hatches in React Can Cause an XSS Attack

A lot of times, you want to get a reference to a DOM element in your React application. React provides you with findDOMNode and createRef as escape hatches. These methods give a direct reference to the DOM elements. Consider the following code:

import './App.css';
import {useEffect, createRef} from 'react';

function App() {
  const divRef=createRef();
  const data="lorem ipsum just some random text"

  useEffect(()=>{
    divRef.current.innerText="After rendering, this will display"
  },[])

  return (
    <div className="App">
      <div className="container" ref={divRef}>
        {data}
      </div>
    </div>
  );
}

export default App;

I have a simple <div> with the ref divRef. When the component's DOM loads, I change the content inside this <div> using the innerHTML property on its ref. An attacker can easily inject some malicious script by overriding the innerHTML of the <div> inside the useEffect. 

The trick here is simple. Don't use innerHTML to mutate DOM at all! This is yet again a similar situation where you're modifying DOM directly. If you are using refs to add some content inside your HTML elements, use innerText instead.

useEffect(()=>{
divRef.current.innerText="After rendering, this will display"
},[])

Now, even if the attacker is able to inject some <script> tags through divRef, it will be rendered as a string in your application. This kind of pattern is rare, and you should always avoid mutating DOM directly using refs.

Key Takeaways

Protecting your React application from XSS is not a one-step process. The best way to safeguard your React application against XSS attacks is to anticipate them early in your codebase. You can then define a set of rules or coding guidelines for your application. All developers on your team can follow these guidelines to ensure that whatever code they write is not prone to XSS or any other vulnerabilities. 

Here's how you can prevent XSS in your application:

  1. Validate all data that flows into your application from the server or a third-party API. This cushions your application against an XSS attack, and at times, you may be able to prevent it, as well.

  2. Don't mutate DOM directly. If you need to render different content, use innerText instead of innerHTML. Be extremely cautious when using escape hatches like findDOMNode or createRef in React.

  3. Always try to render data through JSX and let React handle the security concerns for you.

  4. Use dangerouslySetInnerHTML in only specific use cases. When using it, make sure you're sanitizing all your data before rendering it on the DOM.

  5. Avoid writing your own sanitization techniques. It's a separate subject on its own that requires some expertise.

  6. Use good libraries for sanitizing your data. There are a number of them, but you must compare the pros and cons of each specific to your use case before going forward with one.

You can use the above points as a coding guideline when building React applications.

Add Automated Security Testing to Your Pipeline

Conclusion

As developers, we must learn how to build safe and secure applications. XSS is one of the most common application vulnerabilities that has existed for a long time. We discussed DOM-based XSS attacks, but there are others that are equally important and dangerous. If you wish to dive deeper into XSS, here's an amazing guide that can help you out. 

This post was written by Siddhant Varma. Siddhant is a full stack JavaScript developer with expertise in frontend engineering. He’s worked with scaling multiple startups in India and has experience building products in the Ed-Tech and healthcare industries. Siddhant has a passion for teaching and a knack for writing. He's also taught programming to many graduates, helping them become better future developers.


StackHawk  |  July 18, 2021