Philippe De Ryck

Hi, I'm Philippe, and I help developers protect companies through better web security. As the founder of Pragmatic Web Security, I travel the world to teach practitioners the ins and outs of building secure software.

Want to learn more about OAuth 2.0 and OpenID Connect?

Save yourself days of digging through dozens of specs with this online course

More information

Preventing XSS in React (Part 2): dangerouslySetInnerHTML

Dynamically rendering benign HTML code in React requires the use of dangerouslySetInnerHTML. That is not a naming mistake. This property is dangerous, and using it carelessly will create XSS vulnerabilities in your application. In this article, we discuss why the property is there, how you can use it, and how the Signal messenger misused it. This article is the second in a series of three, and a must-read for every React developer.

28 May 2020 SPA Security XSS, React, Single Page Applications

In the first part of this series, we talked about how React prevents certain XSS vulnerabilities by default. We also discussed the challenge of using dynamically defined URLs in a template, along with the proper mitigation strategy.

In this article, we investigate how to render benign HTML without introducing XSS vulnerabilities.

The use case for dangerouslySetInnerHTML

Real-world applications often run into requirements where they need to render dynamic HTML code. That HTML code typically originates from untrusted sources, such as user-provided data. The image below illustrates one common scenario of this use case.

An image from the CKEditor website illustrating the editor's features
An image from the <a href='https://ckeditor.com/ckeditor-5/' rel='noopener' target='_blank'>CKEditor</a> website illustrating the editor's features

Rich text editors, such as CKEditor, typically generate HTML as output. The document you can see above displays images with <img> elements, headers with an <h1> tag, and paragraphs of text with the <p> tag. To render the output of the editor correctly, the browser needs to be able to parse and render the HTML code.

In my training courses, I use a training application that collects restaurant reviews. The application wants users to be able to use benign HTML constructs in their reviews. While reviews are less elaborate than CKEditor documents, the technical requirements are the same.

When we put the review data into the template with simple data binding, React will ensure the output is properly encoded. The end result looks far from the expected result, as you can see below.

Simple data binding results in the automatic encoding of potentially dangerous characters
Simple data binding results in the automatic encoding of potentially dangerous characters

To avoid the automatic encoding of React, the developer has to assign the data to the innerHTML attribute directly. However, doing so carelessly will cause XSS vulnerabilities. That’s why React exposes the innerHTML attribute through the dangerouslySetInnerHTML property. You can see this property in the code example below.

This code example illustrates how to render a review with HTML


return (<p dangerouslySetInnerHTML={{__html: review}}></p>);

Feeding the same review through this code results in the following output.

Using dangerouslySetInnerHTML enables the rendering of HTML in the data
Using dangerouslySetInnerHTML enables the rendering of HTML in the data

That sure looks good. Unfortunately, the code snippet shown above is insanely insecure. It renders all HTML in the data, regardless of whether the code is benign or dangerous.

Note how React requires that the data is provided in a very specific format. This requirement acts as an additional safeguard, as it ensures that developers will not accidentally use this feature without reading the documentation. That documentation page clearly states that this is a dangerous feature (hence the name dangerouslySetInnerHTML) that should be used with caution.

Below is a proof-of-concept of an XSS attack. Binding the review shown above with dangerouslySetInnerHTML will cause the script code to be executed. Note that the alert() function is harmless, but that an attacker could use the same attack vector to take control of the application in the user’s browser.

User-provided data with benign HTML can also contain dangerous code


This restaurant is absolutely horrible. 
The service is <b>slow</b> and the food is <i>disgusting</i>.
<img src="nonexistent.png" onerror="alert('This restaurant got voted worst in town!');" />

To be honest, I am a bit disappointed in the naming of dangerouslySetInnerHTML. It marks a feature as dangerous, without providing an alternative for handling everyday requirements in modern applications. The only way to output HTML in React is by Using dangerouslySetInnerHTML, but with safe data. Keep reading to learn how to make the use of dangerouslySetInnerHTML safe.

Boost your security skills with a 2-day hands-on React security workshop

Reach out to discuss a practical training course on current best practices

More information

What dangerouslySetInnerHTML is not for

dangerouslySetInnerHTML is not there to make your life easier. It is not intended as a workaround for challenging problems. Even the suggestion from React itself, to use dangerouslySetInnerHTML to sidestep the future blocking of javascript: URLs (see part 1), should be avoided.

React throws a warning when it encounters a dangerous JavaScript URL during development
React throws a warning when it encounters a dangerous JavaScript URL during development

Using dangerouslySetInnerHTML for such use cases results in sloppy and hard-to-maintain code. It also normalizes a dangerous coding pattern, causing significant harm.

With that off my chest, let’s look at how to use dangerouslySetInnerHTML securely.

Sanitization as a defense

Context-sensitive output encoding is not the right defense to allow benign HTML in the output, as it encodes both benign and malicious code constructs. Instead of encoding, the application needs to sanitize the HTML before rendering it. Let’s dive a bit deeper.

An HTML santizer understands HTML. It knows which elements and attributes are safe (e.g., <b> and <i>), and which are potentially unsafe (e.g., <script> and the href attribute of an <a> element). A sanitizer will leave all the safe pieces of HTML in the data, but scrub out all the dangerous parts.

Let’s take the review we used before as an example. The review is included again below and contains both safe and unsafe HTML constructs.

User-provided data is a common attack vector to exploit XSS vulnerabilities


This restaurant is absolutely horrible. 
The service is <b>slow</b> and the food is <i>disgusting</i>.
<img src="nonexistent.png" onerror="alert('This restaurant got voted worst in town!');" />

If we run this review through a sanitizer, it would come out as the review you can see below.

The user-provided review after it has been sanitized by an HTML sanitizer


This restaurant is absolutely horrible. 
The service is <b>slow</b> and the food is <i>disgusting</i>.
<img src="nonexistent.png" />

As you can see, the benign parts of the HTML are still intact. The dangerous parts, however, are taken out by the sanitizer. In this case, the dangerous part is the onerror attribute of the img tag, which is capable of triggering JavaScript execution. If we take the safe review and put it into the page using dangerouslySetInnerHTML, it will not cause a cross-site scripting problem.

Note that sanitization sounds a lot like filtering (a.k.a, the stuff you do with regexes), but it is an entirely different approach. A sanitizer will parse the data into a syntax tree and make security decisions based on that tree. A good sanitizer uses a list of safe values and considers everything that is not on the list unsafe.

But how do you sanitize in React?

Sanitizing HTML in React

First of all, Do not build your own sanitizer. HTML sanitization is extremely tricky and should be left up to experts.

If you look around, you will find HTML sanitizers for virtually every language out there. In this case, we need an HTML sanitizer that runs in JavaScript. While there may be a few options, you only really need one: DOMPurify. DOMPurify is a lightweight and secure HTML sanitizer, built by a german team of XSS experts. FYI, the example of sanitization I showed you before was obtained by running it through DOMPurfiy.

Since React runs on JavaScript, I strongly recommend the use of DOMPurify. DOMPurify is not built into React, so you will need to import it. But first, you will need to install the dom-purify NPM module. Once the module is installed, you can import and use DOMPurify, as shown in the code example below.

The user-provided review after it has been sanitized by an HTML sanitizer


// Import DOMPurify
const DOMPurify = require('dompurify')(window);

// Sanitize the review
return (<p dangerouslySetInnerHTML={{__html: DOMPurify.sanitize(review)}}></p>);

Awesome, right?

While this is simple and effective, I’m not convinced that this pattern is the right approach in real-world React applications. Imagine a company with dozens of developers building React applications. Each of these developers needs to learn about this problem and its solution. Next, code analysis tools need to find every occurrence of dangerouslySetInnerHTML and assess if the output is properly sanitized or not.

Such a process is guaranteed to lead to vulnerabilities, as illustrated in this case with the Signal messenger app. The image below shows a side-by-side of the vulnerability and the fix.

The secure messaging app Signal had to fix a React-based XSS vulnerability
The secure messaging app Signal had to fix a React-based XSS vulnerability

To avoid these issues, I recommend building a small React component that encapsulates the ability to take HTML as input, sanitize it, and assign it to the dangerouslySetInnerHTML property. That way, all developers can rely on this library, instead of having to learn about DOMPurify. Additionally, code scanning tools can flag every use of dangerouslySetInnerHTML outside of the sanitization component.

Angular does something very similar to the last suggestion here. Angular exposes the [innerHTML] property. Any value assigned to that property is automatically sanitized before it is parsed and rendered in the DOM. As a result, it is much easier for Angular developers to avoid this type of XSS. Scanning Angular applications for dangerous coding patterns also becomes easier.

Server-side page rendering

DOMPurify is absolutely great as a sanitizer for JavaScript code running in the browser. DOMPurify heavily relies on the browser’s parser to make sense of the HTML. Therefore, using DOMPurify in server-side code, such as server-side rendered React applications, is a bit more challenging. I ran into this problem when investigating the use of React templates in a hapi application for a customer. The issue was known, and there is a constructive Github thread about it.

When running in server-side (node) environment, DOMPurify relies on jsdom for HTML parsing and tree walking. Updates to jsdom’s feature set enable most features of DOMPurify. Installing both DOMPurify and jsdom should allow you to use DOMPurify in server-side environments as well.

This setup can be simplified by using the isomorphic-dompurify package. This package bundles dom-purify with jsdom, ensuring that they work as expected, even when running in a server-side environment.

Advanced attacks abusing dangerouslySetInnerHTML

Unfortunately, scanning your code for all uses of dangerouslySetInnerHTML is not sufficient. In a more advanced attack scenario, the attacker gains control over an entire props object. A typical example is a user defining certain display preferences, which get persisted as a JSON object in the backend datastore. Next time those settings are used, they are parsed into an object again and used to render an element. The code below illustrates such a scenario.

In this example, user-provided content ends up as the props object in the createElement API call


let userDataFromStorage = `...`;
let userPreferences = JSON.parse(userDataFromStorage);
return React.createElement("container", userPreferences);

The snippet below shows a sample value of the user’s preferences.

The object holding the user's preferences


{
  "style": {
    "color": "green",
    "backgroundColor": "yellow",
    "border": "solid 5px blue"
  }
}

To exploit this vulnerability, all the attacker needs to do is provide a settings object that contains a dangerouslySetInnerHTML property. When the parser sees this property, it will use the provided value as the HTML code for the element being rendered. The code example below illustrates such a payload.

The malicious payload, parsed from a String into a JSON object


{
  "style": {
    "color": "green",
    "backgroundColor": "yellow",
    "border": "solid 5px blue"
  },
  "dangerouslySetInnerHTML": {
    "__html": "<img src='none.png' onerror='alert(\\\"Rainbow mode FTW!\\\")'>"
  }
}

Note that this attack only works on an element without children. If the application already provides content for the container element as the third argument to the createElement API call, React will notice the double content and complain with an error.

Nonetheless, this attack vector is still relevant in modern React applications. The best defense is to avoid using user-provided JSON data directly, or to sanitize the object before using it.

Secure coding guidelines

That brings us to the summary of this second part on Preventing XSS in React. There are three concrete takeaways.

First, you should always sanitize dynamic values that you assign to dangerouslySetInnerHTML. The recommended sanitizer to use is DOMPurify.

Second, you should encapsulate this behavior in a security component and encourage developers to use that component. This allows you to apply code-scanning techniques to flag dangerous uses of the dangerouslySetInnerHTML property.

Third, you should prevent the direct use of user-provided properties when calling the createElement API. Again, code scanning techniques can definitely help flag problematic uses.

Finally, I also built a cheat sheet on preventing XSS in React applications. Make sure you grab a copy and share with your friends and colleagues.

All articles in this series



About Dr. Philippe De Ryck

Hi, I'm Philippe, and I help developers protect companies through better web security. Learn more about my security training program, advisory services, or check out my recorded conference talks.

Want to learn more about OAuth 2.0 and OpenID Connect?

Save yourself days of digging through dozens of specs with this online course

More information
Philippe De Ryck

Dr. Philippe De Ryck

Hi, I'm Philippe, and I help developers protect companies through better web security. As the founder of Pragmatic Web Security, I travel the world to teach practitioners the ins and outs of building secure software.


Talks and workshops

You will often find me speaking and teaching at public and private events around the world. My talks always encourage developers to step up and get security right.


Articles

Security is often about small nuances. In my articles, I dive deeper into various security topics, providing concrete guidelines and advice. My articles also answer questions I often get while speaking or teaching.


Security resources

Getting security right is all about knowledge. I strongly believe in sharing that knowledge to move forward as a community. Among my resources, you can find developer cheat sheets, recorded talks, and extensive slide decks.


Mailing list

Subscribe to the Pragmatic Web Security mailing list to stay up to date on the latest activities and resources.

Subscribe