Dev

By Carlos Santana on
Reading time: 3 minutes

XSS-yao1d.png

In this post, I'll explain you about Cross-Site-Scripting (XSS) vulnerabilities in React. XSS attacks are very common in web applications, and some developers are still not aware of this problem. XSS attacks are malicious scripts that are injected into the DOM of unprotected web applications. The risks can vary with each application. It could just be an innocent alert script injection, or worse, someone can get access to your cookies and steal your private credentials for example.

Let's create an XSS component to start playing with some XSS attacks. We are going to have a response variable that will simulate a response from a real server.

Creating an XSS component

This will be our XSS component:

import React, { Component } from 'react';

// Let's suppose this response is coming from a service and have
// some XSS attacks in the content...
const response = [
  {
    id: 1,
    title: 'My blog post 1...',
    content: '<p>This is <strong>HTML</strong> code</p>'
  },
  {
    id: 2,
    title: 'My blog post 2...',
    content: `<p>Alert: <script>alert(1);</script></p>`
  },
  {
    id: 3,
    title: 'My blog post 3...',
    content: `
      <p>
        <img onmouseover="alert('This site is not secure');" src="attack.jpg" />
      </p>
    `
  }
];

// Let's suppose this is our initialState of Redux
// which is injected to the DOM...
const initialState = JSON.stringify(response);

class Xss extends Component {
  render() {
    // Parsing the JSON string to an actual object...
    const posts = JSON.parse(initialState);

    // Rendering our posts...
    return (
      <div className="Xss">
        {posts.map((post, key) => (
          <div key={key}>
            <h2>{post.title}</h2>
            <p>{post.content}</p>
          </div>
        ))}
      </div>
    );
  }
}

export default Xss;
File: src/components/Xss/Xss.js

Let's render our component:

Xss1-RDJcM.png

Interesting, by default, React prevent us from injecting HTML code directly into our components. It's rendering the HTML as a string. This is great, but sometimes we need to insert HTML code in our components.

Implementing dangerouslySetInnerHTML

This prop is intimidating at the beginning (maybe because it explicitly says the word danger!). I'm going to show you that this prop is not too bad if we know how to use it securely. Let's modify our previous code, and we are going to add this prop to see how the HTML is rendering now:

import React, { Component } from 'react';

// Let's suppose this response is coming from a service and have
// some XSS attacks in the content...
const response = [
  {
    id: 1,
    title: 'My blog post 1...',
    content: '<p>This is <strong>HTML</strong> code</p>'
  },
  {
    id: 2,
    title: 'My blog post 2...',
    content: `<p>Alert: <script>alert(1);</script></p>`
  },
  {
    id: 3,
    title: 'My blog post 3...',
    content: `
      <p>
        <img onmouseover="alert('This site is not secure');" src="attack.jpg" />
      </p>
    `
  }
];

// Let's suppose this is our initialState of Redux
// which is injected to the DOM...
const initialState = JSON.stringify(response);

class Xss extends Component {
  render() {
    // Parsing the JSON string to an actual object...
    const posts = JSON.parse(initialState);

    // Rendering our posts...
    return (
      <div className="Xss">
        {posts.map((post, key) => (
          <div key={key}>
            <h2>{post.title}</h2>
            <p><strong>Secure Code:</strong></p>
            <p>{post.content}</p>
            <p><strong>Insecure Code:</strong></p>
            <p dangerouslySetInnerHTML={{ __html: post.content }} />
          </div>
        ))}
      </div>
    );
  }
}

export default Xss;
File: src/components/Xss/Xss.js

Now the page looks like this: 

Xss2-RbgR4.png

Interesting again, probably you thought that the content of "My blog post 2" will fire an alert in the browser but does not. If you inspect the code the alert script is right there: 

Xss3-zLePz.png

Even if we use dangerouslySetInnerHTML, React try to protect us from malicious scripts injections, but it is not secure enough for us to relax on the security aspect of our site. Now let's see the issue with My blog post 3 content.

The code:

<img onmouseover="alert('This site is not secure');" src="attack.jpg" />

Is not directly using a <script> tag to inject malicious code, but is using an <img> tag with an event (onmouseover). So, if you were happy about React's protection, we can see that this XSS attack will be executed if we move the mouse over the image: 

Xss4-Nkmun.png

Preventing XSS attacks

This is kind of scary, right? But as I said at the beginning, there is a secure way to use dangerouslySetInnerHTML and, yes, as you may be thinking right now, we need to clean our code of malicious scripts before we render it with dangerouslySetInnerHTML. The next code will take care of removing all the <script> tags and events from tags, but of course, you can edit this depending on the security level you want to have:

import React, { Component } from 'react';

// Let's suppose this response is coming from a service and have
// some XSS attacks in the content...
const response = [
  {
    id: 1,
    title: 'My blog post 1...',
    content: '<p>This is <strong>HTML</strong> code</p>'
  },
  {
    id: 2,
    title: 'My blog post 2...',
    content: `<p>Alert: <script>alert(1);</script></p>`
  },
  {
    id: 3,
    title: 'My blog post 3...',
    content: `
      <p>
        <img onmouseover="alert('This site is not secure');" src="attack.jpg" />
      </p>
    `
  }
];

const removeXSSAttacks = html => {
  const SCRIPT_REGEX = /<script[^<]*(?:(?!</script>)<[^<]*)*</script>/gi;

  // Removing the <script> tags
  while (SCRIPT_REGEX.test(html)) {
    html = html.replace(SCRIPT_REGEX, '');
  }

  // Removing all events from tags...
  html = html.replace(/ onw+="[^"]*"/g, '');

  return html;
};

// Let's suppose this is our initialState of Redux
// which is injected to the DOM...
const initialState = JSON.stringify(response);

class Xss extends Component {
  render() {
    // Parsing the JSON string to an actual object...
    const posts = JSON.parse(initialState);

    // Rendering our posts...
    return (
      <div className="Xss">
        {posts.map((post, key) => (
          <div key={key}>
            <h2>{post.title}</h2>
            <p><strong>Secure Code:</strong></p>
            <p>{post.content}</p>
            <p><strong>Insecure Code:</strong></p>
            <p dangerouslySetInnerHTML={{ __html: removeXSSAttacks(post.content) }} />
          </div>
        ))}
      </div>
    );
  }
}

export default Xss;
File: src/components/Xss/Xss.js

If you look at the code now, you will see that now our render is more secure:

Xss5-JuJiP.png

The issue with JSON.stringify

So far, we have learned how to inject HTML code into a React component with dangerouslySetInnerHTML in a secure way, but there is another potential security issue using JSON.stringify. If we have an XSS attack (<script> tag inside the content) in our response then we use JSON.stringify to convert the object to a string, the HTML tags are not encoded. That means if we inject the string into our HTML (like Redux does with the initial state), we will have a potential security issue. The output of JSON.stringify(response); is like this: 

[
  {
    "id": 1,
    "title": "My blog post 1...",
    "content": "<p>This is <strong>HTML</strong> code</p>"
  },
  {
    "id": 2,
    "title": "My blog post 2...",
    "content": "<p>Alert: <script>alert(1);</script></p>"
  },
  {
    "id": 3,
    "title": "My blog post 3...",
    "content": "<p><img onmouseover="alert('This site is not secure');" src="attack.jpg" /></p>"
  }
]

As you can see, all the HTML is exposed without any encoding characters, and that is a potential security issue. But how we can fix this? We need to install a package called serialize-javascript:

npm install serialize-javascript

Instead of using JSON.stringify, we need to serialize the code like this:

import serialize from 'serialize-javascript';

// Let's suppose this response is coming from a service and have
// some XSS attacks in the content...
const response = [
  {
    id: 1,
    title: 'My blog post 1...',
    content: '<p>This is <strong>HTML</strong> code</p>'
  },
  {
    id: 2,
    title: 'My blog post 2...',
    content: `<p>Alert: <script>alert(1);</script></p>`
  },
  {
    id: 3,
    title: 'My blog post 3...',
    content: `
      <p>
        <img onmouseover="alert('This site is not secure');" src="attack.jpg" />
      </p>
    `
  }
];

// ...

const initialState = serialize(response); 
console.log(initialState);
File: src/components/Xss/Xss.jss

The output of the console is this:

[
  {
    "id": 1,
    "title": "My blog post 1...",
    "content": "u003Cpu003EThis is u003Cstrongu003EHTMLu003Cu002Fstrongu003E codeu003Cu002Fpu003E"
  },
  {
    "id": 2,
    "title": "My blog post 2...",
    "content": "u003Cpu003EAlert: u003Cscriptu003Ealert(1);u003Cu002Fscriptu003Eu003Cu002Fpu0 03E"
  },
  {
    "id": 3,
    "title": "My blog post 3...",
    "content": "u003Cpu003Eu003Cimg onmouseover="alert('This site is not secure');" src="attack.jpg" u002Fu003Eu003Cu002Fpu003E"
  }
]

Now we have our HTML encoded instead of directly having HTML tags, and the good news is that we can use JSON.parse to convert the string again into our original object.

Only members can see all the codes
You can Login or Sign Up

As I mentioned at the beginning of the post, the XSS attacks are widespread, and many websites are suffering from this problem without knowing it. There are others injections attacks, such as SQL injections, that could happen in an API if we don't take minimal security precautions.

Security recommendations

  • Always sanitize the users' content that comes from forms.
  • Always prefer to serialize instead of JSON.stringify.
  • Use dangerouslySetInnerHTML only when absolutely necessary.
  • Do unit tests for all your components, and try to cover all the possible XSS attacks that some user could do.
  • Always encrypt the passwords with sha1 and md5 (together), and also add a salt value (for example, if the password is abc123, then your salt can be encrypted like this: sha1(md5('$4lT3xt_abc123')).
  • If you use cookies to store sensitive information (personal information and passwords mainly), you can save the cookie with Base64 to obfuscate the data.
  • Add some protection to your APIs (using security tokens) unless you need to have a public API.

I hope you liked this post, if you want to learn more about React and security you can get my React Cookbook!

avatarLeave a comment

Your comment

Only members can comment. You can Login or Sign Up