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 3): escape hatches and component parsers

Preventing XSS in React is manageable when you stay within the boundaries of the framework, but becomes hard once you step out of React's safe zone. In this article, we take a closer look at escape hatches and component parsers and all the reasons you should avoid them. Read on to discover the next level of XSS in React applications.

14 June 2020 SPA Security XSS, React, Single Page Applications

As a component framework, React handles all of the dirty details of putting data into the DOM. Components rely on the React APIs or the JSX templating language to define what should be rendered, and React takes care of it. Under the hood, React instructs the browser to create proper elements and update the DOM.

As discussed in part 1, React automatically ensures the safety of data through simple data binding. In in part 2, we discussed how to output HTML through React components using the dangerouslySetInnerHTML property. Following the secure coding guidelines from the previous two articles will help you build more secure React applications. However, there are a few more exotic cases we haven’t discussed yet.

In this article, we take a closer look at escape hatches and component parsers.

I want to break free!

Generally speaking, React abstracts away the details of the browser’s DOM and offers a higher-level API to render components. Unfortunately, that higher-level API is not always enough. In certain scenarios, developers need to have direct access to the native DOM elements. Examples include managing focus or text selection, or using third-party DOM libraries.

To support such scenarios, React offers an escape hatch, which provides the application direct access to native DOM elements. With that direct access, the application can perform the desired operation, without requiring explicit support from React. In React, two concrete escape hatches give access to native DOM elements: findDOMNode and createRef.

The problem with such an escape hatch is that it returns native DOM elements, with their full API. Consequently, the application obtains the power to manipulate the element directly, without having to go through React. These direct interactions can lead to XSS if they are not used carefully.

The code snippet below shows the findDOMNode escape hatch, along with an XSS vulnerability through the use of innerHTML.

Using the findDOMNode escape hatch to access native DOM elements


import React from 'react';
import ReactDOM from 'react-dom'

class MyComponent extends React.Component {
    constructor(props) {
        super(props);
        this.placeholder = "Hello!";
    }

    componentDidMount() {
        let realData = "... load user-provided data from data store ...";
        ReactDOM.findDOMNode(this).innerHTML = realData;
    }

    render() {
        return (
            
{this.placeholder}
) } } export default MyComponent;

The second escape hatch uses refs to obtain a reference to a DOM element. The code example below illustrates how to use refs and how the use of innerHTML can cause XSS vulnerabilities.

Using createRef to build an escape hatch to access native DOM elements


import React from 'react';

class MyComponent extends React.Component {
    constructor(props) {
        super(props);
        this.placeholder = "Hello!";
        this.myComponent = React.createRef();
    }

    componentDidMount() {
        let realData = "... load user-provided data from data store ...";
        this.myComponent.current.innerHTML = realData
    }

    render() {
        return (
            <div ref={this.myComponent}>{this.placeholder}</div>
        )
    }
}

export default MyComponent;

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

Securing escape hatches

These escape hatches in React give the application direct access to native DOM elements. The innerHTML property used in the assignment is the native DOM innerHTML property. Consequentially, the strategies to secure these use cases are the same strategies traditional JavaScript code needs to apply to prevent DOM-based XSS attacks.

Concretely, there are three strategies to follow:

  • Only output text, and no HTML
  • Use the proper DOM APIs to generate HTML nodes
  • Feed data through DOMPurify before placing it into the page.

Let’s take a closer look at each of these.

Text output

Assigning data to innerHTML instructs the browser to parse and render that data. As a result, the browser will treat embedded HTML as code, potentially causing a DOM-based XSS vulnerability.

Often, developers use innerHTML out of habit, even when the browser is not supposed to treat the data as HTML code. For those scenarios, using the innerText property suffices to avoid injection vulnerabilities. innerText instructs the browser to simply use the data as text, regardless of its contents.

If the data contains any HTML characters, they will simply be displayed to the user, not rendered by the browser. The effect of using innerText is the same as using simple data binding, as we discussed in the first part of this series.

The code example below shows how to use innerText in practice.

Strategy 1: Putting text into the page instead of code


componentDidMount() {
    let realData = "... load user-provided data from data store ...";
    ReactDOM.findDOMNode(this).innerText = realData;
} 

Proper DOM APIs

Another common use case for innerHTML is an application putting static HTML into the page. The example below illustrates such a scenario with a warning dialog.

Strategy 1: Putting text into the page instead of code


componentDidMount() {
    let staticData = `<p class="alert alert-warning">Some warning goes here</p>`;
    ReactDOM.findDOMNode(this).innerText = staticData;
} 

In such scenarios, innerHTML works as intended. It will make sure the browser picks up the HTML and render the warning dialog. While this code example does not have an XSS vulnerability, it remains a dangerous coding practice. All it takes is one refactoring of the code or one copy/paste operation into another component, to create a vulnerability. Take a look at the subtle change in the code example below.

Strategy 1: Putting text into the page instead of code


componentDidMount() {
    let staticData = `<p class="alert alert-warning">New review for your restaurant: ${review.title}</p>`;
    ReactDOM.findDOMNode(this).innerText = staticData;
} 

The application is now placing untrusted data into the page using innerHTML, making it vulnerable to DOM-based XSS.

Since the generated HTML is static and known upfront, the developer can choose to use proper DOM APIs to create the elements. Doing so avoids using a text-to-code sink that invokes the browser’s HTML parser, which sidesteps the entire issue. The code example below illustrates how to use the DOM APIs to generate the same warning.

Strategy 2: Outputting code with the proper DOM APIs


componentDidMount() {
    let warning = document.createElement("p");
    warning.setAttribute("class", "alert alert-warning");
    warning.innerText = `New review for your restaurant: ${review.title}`;
    ReactDOM.findDOMNode(this).appendChild(warning);
} 

Using DOMPurify

Finally, applications often need to output user-provided data with the ability to render HTML. Since the structure of that HTML is not known upfront, the application will have to rely on the browser’s text-to-code features to enable this behavior. To ensure that the data does not trigger script execution or other dangerous features, the application should sanitize the data before putting it into the page.

In the previous article, we discussed how to use DOMPurify with dangerouslySetInnerHTML. Since DOMPurify is an HTML sanitizer, we can use it here as well. The code example below illustrates how to assign data to innerHTML safely using DOMPurify.

Strategy 3: Sanitizing the data with DOMPurify before putting it into the page


const DOMPurify = require('dompurify')(window);
...
componentDidMount() {
    let realData = "... load user-provided data from data store ...";
    ReactDOM.findDOMNode(this).innerHTML = DOMPurify.sanitize(realData);
} 

From text to React

React applications often need to write snippets of HTML into the page. One example we discussed before is untrusted user data containing benign HTML. Other examples include dynamic application behavior or custom error messages. The straightforward solution is to write the data into the page using dangerouslySetInnerHTML. However, doing so offers a developer little control over that data and may seem a bit odd for static code snippets.

That’s why you find articles like this one, suggesting the use of a React component parsing library. Such a library takes a snippet of HTML as input and returns a proper set of React components as output. These components represent the same HTML, but in a way that makes sense for React. As a result, these components can be written into the page without the use of dangerouslySetInnerHTML. The code snippet below illustrates this behavior.

This code uses the component parser to render a static snippet of HTML


render () {
    let message = `

Invalid username or password.

`; return (
{ ReactHtmlParser(message) }
) }

The code snippet shown here is secure. Static data is parsed into components, which are placed into the template. However, these component libraries are not security libraries. They are not equipped to prevent XSS, making them unsuited to handle untrusted data. The code examples below illustrate the exact nature of the vulnerability, along with a potential payload to trigger the XSS vulnerability.

This code uses the component parser on untrusted data, creating an XSS vulnerability


render () {
    let message = `

Invalid username (${username}) or password.

`; return (
{ ReactHtmlParser(message) }
) }

The username value shown here will trigger the vulnerability in the code snippet above


Philippe<iframe src='javascript:alert(1)'></iframe>

To be fair to the article I referenced before, they never recommend to use the html-react-parser package for handling untrusted content. However, without proper context, such a mistake is easy to make. So let’s look at a more secure approach to using a component parsing library.

Securely parsing React components

A React component parser transforms string-based HTML into proper React DOM elements. Since these libraries are intended to be used on static snippets of code, they are not automatically looking for potentially dangerous HTML constructs. As a result, using them on untrusted data creates XSS vulnerabilities.

The only way to prevent such attacks is to ensure the component parser only allows safe elements in the output. Since there are no component parsers with such built-in functionality, the best approach is to combine DOMPurify with a component parsing library. The code snippet below shows how first to sanitize the HTML snippet containing user input before feeding into the React component parser.

This code avoids XSS by sanitizing the HTML snippet before passing it to the component parser


render () {
    let message = `

Invalid username (${username}) or password.

`; return (
{ ReactHtmlParser( DOMPurify.sanitize(message) ) }
) }

By running DOMPurify first, we can ensure that the HTML being fed into the component parser does not contain any dangerous code constructs. Because of that guarantee, we can be confident that the resulting components will not trigger an XSS vulnerability in the browser.

Note that DOMPurify is only needed when untrusted data is used in the HTML code snippets. Statically defined code snippets do not require a separate sanitization step.

Secure coding guidelines

To conclude this article, and the three-part series on React (See part 1 and part 2), let’s wrap this up into three concrete secure coding guidelines.

First, avoid direct DOM manipulation through native DOM elements. The innerHTML property of native DOM elements should not be used directly. When direct DOM output is required, either use proper DOM APIs to create the necessary elements or rely on DOMPurify to sanitize the HTML before writing it into the page.

Second, avoid the use of React component parsers on HTML snippets containing untrusted input. When such functionality cannot be avoided, DOMPurify must sanitize the HTML before parsing it with the component parser.

Finally, whenever you need these features, make sure to wrap them up into a security library. Doing so ensures that the codebase uses an approved implementation, making it easier to scan the codebase for potentially insecure coding patterns.

All articles in this series

Acknowledgements

Ron Perris gave an excellent talk at OWASP AppSec Global DC 2019 covering escape hatches and other security risks in React.



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