About michelada.io


By joining forces with your team, we can help with your Ruby on Rails and Javascript work. Or, if it works better for you, we can even become your team! From e-Commerce to Fintech, for years we have helped turning our clients’ great ideas into successful business.

Go to our website for more info.

Tags


Render dangerous content with React

3rd February 2020

FYI All the content of this post suits best for some edge cases, and the solution proposed here is probably not the more safe way to do it.

Cross-site Scripting (XSS Attacks)

Among all the web vulnerabilities, one of the most common is cross-site scripting, this type of vulnerability allows the attackers to inject scripts on the page to get access to any sensitive information the browser and the site are sharing (cookies, tokens, etc...).

This attack occurs when the data entered is coming from an untrusted source or the data that is sent to the user include dynamic content without been validated first.
Although there are limitless varieties of XSS attacks, Javascript XSS attacks seems to be popular among hackers.

Types of XSS Attacks

There are 3 types of XSS attacks:

Stored XSS Attacks occurs when the injected script is store on the server (i.e. store on a database) so each time the user request a something from the server
the malicious script is sent to the client.

Reflected XSS Attacks happens when the malicious script is reflect on the web that's vulnerable, this could be due to a click on an email link that's malformed or any other external source.

DOM Based XSS Attacks is a vulnerability that occurs on the DOM (Document Object Model) instead of the HTML.

Let's say you have this code on your app:

<script>
   document.write('<h1>My URL:</h1>: '+ document.baseURI);
</script>

Now, imagine someone visits your site using the URL https://www.nicesite.com/index.html#<script>alert('test')</script>, the script will be executed because the code above writes whatever comes on url to the document using document.write.

We can point one of the main differences between this type of XSS attack and the Stored and Reflected: The servers can't stop this attack, since the hash (#) part of the url is not being sent to the server on the request.

Prevent XSS Attacks

For most of the XSS attacks the solution is simple, just sanitize your input data, even if comes from a trusted source.
Doing this will ensure that no matter what's the input or output, is always secure.

Javascript offers us plenty ways to interact with the DOM, so we can works with dynamic content in an easy way, but we need to be careful on how to use it, since it can make vulnerable our websites.

Inputs & Outputs

Here's a tiny list of the most common input and outputs that can be dangerous to use with javascript.

INPUTS:

  • document.URL
  • document.documentURI
  • location.href

OUTPUTS:

  • document.write
  • element.innerHTML
  • element.src (in some elements like img)

React and cross-site scripting

Nowadays all the web apps required some dynamism, from having a multiple step form that shows different question depending on your answers to a simple tables that filter out information, this is where Javascript enters to the equation.

Back in time, when Javascript vainilla was enough to get everything done (which still is, we just 'syntax-sugar' it), one of the way you could handle insertion of dynamic content, was using innerHTML property.

Element.innerHTML:The Element property innerHTML gets or sets the HTML or XML markup contained within the element

So, you can set the HTML content from an element using this property, but what happen when the content has an script inside?

const content = 'Christofer'
el.innerHTML = content


const newContent = "<script>alert('You've been hacked')</script>";
el.innerHTML = newContent

The first 2 lines create a variable that holds a plain string, then using innerHTML set the content of an element to be this value, so far so good, nothing harmless here.

On the next 2 lines of code we do the same, but this time, the string value is html-like with a <script> tag within it, so what would you think will be the output?

Well, if you though that this will result on an alert prompting to the user that he has been hacked, well you are wrong.

HTML5 specifications says that scripts inserted using innerHTML shouldn't execute.


Easy to be safe

React follows the philosophy "easy to be safe", that's why we as developers should be explicit if we want to go for the unsafe path, and this is the case for the dangerouslySetInnerHTML prop.

This prop allows you to inject dynamic html to an element, all you need to do is pass and object with a single property: __html, with a string html-like of what you want to render:

function App() {
  const html = `
    <div>
      <h1> Injected html</h1>
    </div>
  `

  return (
    <div  dangerouslyInnerHTML={{ __html: html }}/>
  )
}

As you can see, seems a little odd that you have to pass an object when it could be a simple string, but this is done intentionally, to remind you that it's dangerous and you should avoid using it as much as possible.

innerHTML vs dangerouslySetInnerHTML

Writing React doesn't mean that you can't use the features that Javascript offer us, you can use innerHTML to add the dynamic html to a react component and it will work the same (both will update the node with the html), but it can lead to undesired performance issues.

React uses a virtual DOM and a diffing algorithm to compare what's been updated and re-render the content, this process is called reconciliation.

Using dangerouslySetInnerHTML you can tell React to ignore all the dynamic html during reconciliation

When you use innerHTML to set the content, all the dynamic html that was generated is included in the reconciliation process, aside performance issues, React could wrongly update the dynamic html.

Since both properties works the same (in fact dangerouslySetInnerHTML implements innerHTML to set the content) they both share kinda the same vulnerabilities, hence the importance of sanitizing your input sources.

Render the danger

Now what happens when you want to use dangerouslySetInnerHTML but also need to execute any script tag that comes inside the html? That's against HTML5 specifications, but if we dig a little bit more on what innerHTML do to inject the html we can found something interesting:

The specified value is parsed as HTML or XML (based on the document type), resulting in a DocumentFragment object representing the new set of DOM nodes for the new elements.

This DocumentFragment is a lightweight version of the document, it can have child nodes, the main difference is that since is a fragment, is not actually a part of the active/main document.

We can create a DocumentFragment using the document.Range API.

const html = `
  <h1>Fragment</h1>
`
const node = document.createRange().createContextualFragment(html);

This code snippet will create a DocumentFragment object, parse the value of the html variable and store the result on a variable called node. All we have to do is render this variable:

element.appenChild(node)

If we translate all of this to a React component we end up with something like this:

import React, { useEffect, useRef } from 'react'

// InnerHTML component
function InnerHTML(props) {
  const { html } = props
  const divRef = useRef(null)

  useEffect(() => {
    const parsedHTML = document.createRange().createContextualFragment(html)
    divRef.current.appendChild(parsedHTML)
  }, [])


  return (
    <div ref={divRef}></div>
  )
}

// Usage
function App() {
  const html = `
    <h1>Fragment</h1>
  `

  return (
    <InnerHTML html={html} />
  )
}

This way we could pass a string with html content that includes <script> tags, and those will be executed  (works with <script> .. content .. </script> and <script src="file.js" />)

dangerously-set-html-content is a tiny (297B Gzipped), no-dependencies, library that allows you to render dynamic html and execute any scripts tag within it.

1) Add it to your project:

yarn add dangerously-set-html-content
// or
// npm install dangerously-set-html-content --save

2) Start using it:

import React from 'react'
import InnerHTML from 'dangerously-set-html-content'

function App() {
  const html = `
    <div>
      <h1>Fragment</h1>
      <script>
        alert('this will be executed');
      </script>
    </div>
  `

  return (
    <InnerHTML html={html} />
  )
}

Of course this doesn't prevent any attack (in fact, does the opposite of that), but sometimes this functionally could be what you are looking for.

I built this because I actually needed this functionality.

Conclusions

All the web is full of vulnerabilities that can cause you headaches if you are not aware of how to prevent them. Mostly of the common frontend libraries already handle a few of these in a way, so you don't have to worry about it‌ but still is good to know what we are dealing with as front-end developers.

Additionally of what React offers us, there are several techniques that can help you to prevent an attack, so if you are having a problem of this type just head to the docs and you'll probably find the solution.

While 99% of the time all this magic behind React works perfectly for us, sometimes we can found ourselves struggling with it, but in the end is just Javascript so embracing both will help us to find the solution to our problem.

Thanks!

View Comments