Preventing XSS in React Applications

Preventing XSS in React Applications

What is the XSS?

Cross-site scripting (XSS) accounts for the majority of web applications security vulnerabilities. It lets attackers inject client-side scripts into web pages, bypass access controls, steal sessions, cookies, connect to ports or computers camera.

Why we need to prevent XSS in React Applications?

React is a popular framework for building a modern JS frontend application. By default, data binding in React happens in a safe way, helping developers to avoid Cross-Site Scripting (XSS) issues. However, data used outside of simple data bindings often results in dangerous XSS vulnerabilities. This blog gives an overview of secure coding guidelines for React.

JSX Data Binding

To fight against XSS, React prevents render of any embedded value in JSX by escaping anything that is not explicitly written in the application. And before rendering it converts everything to a string.

A good example of how React escapes embedded data if you try to render the following content:

function App() {
  const userInput = "Hi, <img src='' onerror='alert(0)' />";

  return (
    <div>
      {userInput}
    </div>
  );
}

The output in the browser will be: Hi,, rendered as a string with an image tag being escaped. That is very handy and covers simple cases where an attacker could inject the script. If you would try to load the same content directly in the DOM, you would see an alert message popped out.

Be careful when using dangerouslySetInnerHTML

Simple data binding does not work when the data needs to be rendered as HTML. This requires a direct injection into the DOM for data to be parsed as HTML, and it can be done by setting dangerouslySetInnerHTML:

const userInput = <b>Hi React</b>;
return <div dangerouslySetInnerHTML={{ __html: userInput }} />;

Without adequate security, rendering HTML causes XSS vulnerabilities. Always ensure the output is properly sanitized.

This will solve business requirements that let user directly style the text, but at the same time it opens a huge risk and possibility for XSS atacks. Now if the evil user enters <b>"Hi, <img src='' onerror='alert(0)' />"</b> the browser will render this:

xss vulnerability

To avoid running dangerous scripts they should be sanitized before rendering. The best option is to use a 3rd party library, for example, popular and maintained library dompurify with zero dependencies sanitizes HTML. Improved code would now:

import createDOMPurify from "dompurify";
const DOMPurify = createDOMPurify(window);

function App() {
  const userInput = "<b>Hi, <img src='' onerror='alert(0)' /></b>";

  return (
    <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userInput) }} />
  );
}

When the developer forgets this, we can improve by ESLint rule:

$ npm i eslint eslint-plugin-jam3 -save-dev

Extend plugins the .eslintrc config file by adding jam3

The plugin will check that the content passed to dangerouslySetInnerHTML is wrapped in this sanitizer function

{
  "plugins": [
    "jam3"
  ],
  "rules": {
    "jam3/no-sanitizer-with-danger": [
      2,
      {
        "wrapperName": ["your-own-sanitizer-function-name"]
      }
    ]
  }
}

XSS when using html-react-parser

Libraries such as html-react-parser enable the parsing of HTML into React elements. These libraries avoid certain XSS attack vectors, but do not offer a reliable security mechanism. Always sanitize data with DOMPurify.

Avoid relying on HTML parsing libraries for security.

return (<div>{ ReactHtmlParser(data) }</div>);

Without proper sanitization, this pattern creates XSS issues.

Handling dynamic URLs

URLs derived from untrusted input often cause XSS through obscure features, such as the javascript: scheme or data:text/html scheme. Dynamic URLs need to be vetted for security before they are used.

  • Never allow unvetted data in an href or src attribute
return ( <a href={data}>Click me!</a> );
  • If possible, hardcode the scheme / host / path separator

    var url = “https://example.com/” + data
    

    This pattern guarantees a fixed destination for this URL

  • Use a URL sanitization library to sanitize untrusted URLs

  • Use DOMPurify to output HTML with dynamic URLs: DOMPurify removes HTML attributes that contain unsafe URLs

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.

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>

Accessing native DOM elements

Traditional web applications suffer from DOM-based XSS when they insecurely insert data into the DOM. React applications can create similar vulnerabilities by insecurely accessing native DOM elements.

  • Avoid DOM manipulation through insecure APIs: innerHTML and outerHTML often cause DOM-based XSS

  • Scan your codebase for references to native DOM elements:

    • React’s createRef function exposes DOM elements
    • ReactDOM’s findDOMNode function exposes DOM elements
  • When DOM manipulation cannot be avoided, use safe APIs E.g., document.createElement instead of innerHTML

Reference: