Mitigating XSS attacks in React applications

Omal Vindula
10 min readApr 6, 2023

--

Photo by Lautaro Andreani on Unsplash

What is XSS?

Cross-site scripting (A.K.A XSS) is a web security vulnerability that allows an attacker to inject malicious code into otherwise benign and trusted websites. This will allow the attacker to execute malicious code into a web application and the attack occurs when the victim uses the web application that contains malicious code. One of the most common causes that enable XSS attacks is when the web application does not sanitise user input and the output it generates from these inputs.

Typically there are two stages for an XSS attack.

  1. The attacker injects the malicious code into the web application — Attacker adding a comment containing malicious JS code in a forum which does not have XSS protection.
  2. The victim using the vulnerable web application with malicious code — Victim visiting the forum and loading the comment section will execute the malicious code in the comment. Typically the malicious code will steal some user data/tokens and will post it to the attacker.

A basic example of an XSS attack is when an attacker can input Javascript code such as <script>alert(“You’ve been hacked!”)</script> to a web application where it displays these inputs without any sanitisation. This will embed the malicious JS code within the DOM of the application which allows the attacker to exploit the application.

Another form of an XSS attack is embedding the JavaScript code indirectly via an <img> tag as follows.

<img onerror='alert(\"You've been hacked!\");' src='invalid-image' />

This will render an image which will be broken as the src contains an invalid image URL. Since image is invalid, it will make the <img> to execute the onerror function which contains the malicious code.

Types of XSS

There are many different ways to classify XSS attacks. But in our case, I will consider 2 types of XSS.

  1. Client XSS — Client XSS occurs when untrusted user-supplied data is used to update the DOM with an unsafe JavaScript call. A JavaScript call is considered unsafe if it can be used to introduce valid JavaScript into the DOM. The source of this data could be from the DOM, or it could have been sent by the server (via an AJAX call, or a page load). The ultimate source of the data could have been from a request, or from a stored location on the client or the server. [1]
  2. Server XSS — Server XSS occurs when untrusted user-supplied data is included in an HTTP response generated by the server. The source of this data could be from the request, or from a stored location. In this case, the entire vulnerability is in server-side code, and the browser is simply rendering the response and executing any valid script embedded in it. [1]

Since this article is based on React, I will mainly focus on the Client XSS type of attacks.

Why XSS?

Like previous examples, the attacker can run some JavaScript code in the client application which is mostly harmless other than disrupting its normal operation and most web browsers run JavaScript in a very tightly controlled environment. JavaScript has limited access to the user’s operating system and the user’s files. Because of these factors, XSS attacks may not look serious on the surface. But XSS attacks can be deadly on the right hands.

  1. Javascript has access to the objects in the web application. This includes cookies and objects in the session/local storage such as tokens which can be used to impersonate a user and gain access to their data.
  2. JavaScript in modern browsers can use HTML5 APIs. For example, it can gain access to the user’s geolocation, webcam, microphone, and even specific files from the user’s file system. But most of these require explicit permission from the user and it will be difficult for the attacker to gain access to these resources if the user is vigilant.
  3. JavaScript can use the XMLHttpRequest object to send HTTP requests with arbitrary content to arbitrary destinations.
  4. The attacker can read and manipulate the DOM of the application which is less harmful other than disrupting the web application.

Out of these factors, XSS is widely used to steal tokens such as access tokens, and refresh tokens since most modern web applications store them in session storage and Javascript can easily access these tokens by design.

Is React safe from XSS attacks?

I would say React is “pretty safe” from XSS attacks. There are many safety measures such as auto-escaping variables included in React.js to overcome these attacks.

For example, it is very common in React applications to append data to the DOM as follows.

<h1>{ someUserInputValue }</h1>

One may think that an attacker could input a malicious code to the DOM and perform XSS as follows.

const someUserInputValue = '<script>alert(“This is an XSS attack”)</script>';

return <h1>{ someUserInputValue }</h1>

Typically this attack would work in traditional HTML, JS based application. But React is clever! Instead of executing the malicious code, it would interpret the code as a string and it would look like this.

React interprets code inputted to DOM as string

Other than looking ugly in the UI, this code cannot do much damage to the application as it is a mere string! So are we completely safe from XSS attacks in React? Not quite… we cannot guarantee 100% safety as large part of this depends on the practices followed by the developer when they build the React application. So let’s look at some common mistakes that allow attackers to perform XSS on a React application and how to mitigate those attacks by following the best practices.

XSS attack vectors in React​

Application in question

To emulate these attacks, I will be using a simple to-do application made with React and this is made to be intentionally vulnerable to the XSS attacks for demonstration purposes. By not doing the things I have done in this application, you should be able to mitigate XSS attacks in your React application.

Sample To-do Application made with React

For demonstration purposes I have saved a random string as access_token in the session storage of this application. We will try to capture this object from different XSS attack vectors.

access_token stored in the session storage

1. XSS via dangerouslySetInnerHTML

As the name suggests, dangerouslySetInnerHTML is dangerous! Avoid using this method as much as you can as this will be a primary vector for XSS attacks. Let's use dangerouslySetInnerHTML in our to-do application and see what happens.

<List
className="list"
bordered
itemLayout="horizontal"
dataSource={todoArray}
renderItem={(item, index) => (
<List.Item>
<Row className="list-row">
<Col
span={20}
className="list-itemname"
>
<Checkbox
onChange={() => checkTodo(index)}
checked={item.checked}
className={item.checked? "checked-item": ""}
>
<div dangerouslySetInnerHTML={{"__html": item.name }}></div>
</Checkbox>
</Col>
<Col span={4}>
<a
className="delete-item"
href
onClick={() => deleteTodo(index)}
>
Delete
</a>
</Col>
</Row>
</List.Item>
)}
/>

What this code does is that it will go through the to-do list items and will render a list of items. The caveat here is <div dangerouslySetInnerHTML={{"__html": item.name }}></div> . Because of this single line of code, the user can input any Javascript code as a to-do list item and it will get executed!

If we enter the following as a to-do list input:

 <img onerror=’console.log(sessionStorage.getItem(“access_token”))’ src=’invalid-image’ />

The result in the application would look like this -

XSS Attack on the React Application

This looks innocent on the surface with only a broken image icon in the UI. But the browser console will output the access token from session storage as shown below!

The attacker have retrieved your access_token and can do whatever they want with it!

Because dangerouslySetInnerHTML was used, the attacker have gained your access token. In a serious XSS attack, the malicious code will not only retrieve the access token, but will post it to the attacker (or some server of the attacker) so that they can impersonate you and gain access to your data.

But this can be avoided by not using dangerouslySetInnerHTML and using the following approach.

<div>
{ item.name }
</div>

Using the above approach will convert the malicious code to a string (as mentioned in the very beginning) and will not be able execute, thus saving you from XSS attacks. But there can be instances where you have to use this method render some HTML content as it is, without converting it to a string. In such rare cases, you must sanitise data that contains HTML elements before rendering it on the DOM. There are a number of libraries out there that you can use. One such library is DOMPurify.

Example Code — https://github.com/DonOmalVindula/sample-to-do-app/tree/xss-dangerously-set-inner-html

2. XSS via href attribute

If we allow users to pass inputs into href attributes in <a/> tags (or by using location.href property), this will also be an opportunity for an attacker to attempt an XSS attack with the combination of javascript:<some-malicious-code>. Consider the following code:

<List
className="list"
bordered
itemLayout="horizontal"
dataSource={todoArray}
renderItem={(item, index) => (
<List.Item>
<Row className="list-row">
<Col
span={20}
className="list-itemname"
>
<Checkbox
onChange={() => checkTodo(index)}
checked={item.checked}
className={item.checked? "checked-item": ""}
>
<a href={ item.name }>{ item.name }</a>
</Checkbox>
</Col>
<Col span={4}>
<a
className="delete-item"
href
onClick={() => deleteTodo(index)}
>
Delete
</a>
</Col>
</Row>
</List.Item>
)}
/>

In this example we have <a href={ item.name }>{ item.name }</a> instead of <div dangerouslySetInnerHTML={{"__html": item.name }}></div> to display the to-do list item. On the surface this looks pretty harmless. But when the href is combined with javascript: keyword, the attacker can execute almost any JS code when the <a> tag is clicked.

If we enter the following as a to-do list input:

javascript:alert(sessionStorage.getItem("access_token"))

The result in the application would look like this -

Before XSS attack via href attribute

When a user clicks on this to-do list item, the malicious code in the href will be executed and the attacker can view the access token as follows.

After XSS attack via href attribute

There is no special way to mitigate these attacks other than avoiding user inputs used as href attributes. If it is unavoidable, you can still sanitise the user input before binding it with the href attribute using a library such as DOMPurify.

React won’t save you from javascript: URIs, which is certainly something you must keep in mind.

Example Code — https://github.com/DonOmalVindula/sample-to-do-app/tree/xss-href-exploit

3. XSS via user-defined props

A common practice I’ve seen in React applications is that the props are sent using the ... operator as follows.

<SomeReactComponent (...props} />

There’s nothing wrong with this method except when these props can be defined by the user. Then the attacker can exploit this and send a some props as follows.

const userDefinedProps = {
dangerouslySetInnerHTML: {
"__html": "<img onerror='alert(sessionStorage.getItem(\"access_token\"));' src='invalid-image' />"
}
};

<div {...userDefinedProps} />

The resulting code that will be rendered in the DOM will be as follows.

<div
dangerouslySetInnerHTML={{
"__html": "<img onerror='alert(sessionStorage.getItem(\"access_token\"));' src='invalid-image' />"
}}
/>

This code is similar to the case 1 we discussed as this will use the dangerouslySetInnerHTML indirectly to perform an XSS attack.

This can be avoided by not passing props directly into component (especially when they can be defined by the user) and using interfaces to structure the props so that the attacker cannot include unwanted props into React components.

Example Code — https://github.com/DonOmalVindula/sample-to-do-app/tree/xss-user-defined-props

4. XSS via eval() in JavaScript

The eval() function evaluates JavaScript code represented as a string and returns its completion value. The source is parsed as a script. [2]

As the definition suggestions, the eval() function can execute malicious code specially if some user input is passed to the eval() function. Therefore never use eval()! It is a HUGE security risk and there are better alternative approaches other than eval(). Just for the sake of demonstration, consider the below code where the user input is sent to eval() function. (for some mysterious reason)

const addTodo = () => {
eval(todoItem);
if (todoItem) {
const todoObject = {
name: todoItem,
checked: false
};

setTodoArray([...todoArray, todoObject]);
}
form.resetFields();
}

In here, the user input is sent to the eval() function without any sanitisation. That means the attacker can input any JavaScript code and it will get executed by the eval() function even before adding the item to the to-do list.

If I input the following malicious code:

javascript:alert(sessionStorage.getItem("access_token"))

This will be the result -

To avoid this attack vector, simply don’t use the eval() function as it does more harm than good for your application.

Example Code — https://github.com/DonOmalVindula/sample-to-do-app/tree/xss-eval-exploit

Summary

You can mitigate the XSS attacks by following the above practices. But the journey doesn’t end there. As an additional layer of security, you can utilize a Content-Security Policy (CSP) [3] to detect and mitigate XSS attacks. You can read more about the usage of CSP in-depth in this article. [6]

References

[1] — https://owasp.org/www-community/Types_of_Cross-Site_Scripting

[2] — https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval

[3] — https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP

[4] — https://reactjs.org/

[5] — https://github.com/DonOmalVindula/sample-to-do-app

[6] — https://stackoverflow.com/collectives/wso2/articles/75138763/using-csp-to-reinforce-your-react-application-against-xss-attacks

--

--