• Home
  • React Security Vulnerabilities and How to fix them

As one of the most popular JavaScript libraries, React has become an industry standard for developing single-page and mobile applications, and personally, the programming library that I love the most. It’s so simple but complex, easy to learn, and structured: with no surprise, I can see everywhere popping up like mushrooms React apps.
However, just like any other technology, it’s not immune to potential security vulnerabilities; knowing them will allow you to be prepared and prevent one or more to affect your work and website.

1. The most common: Cross-Site Scripting (XSS) Attacks

One of the most common security threats in any web application is Cross-Site Scripting (XSS), which occurs when an attacker injects malicious scripts into web pages viewed by other users, potentially causing damage or compromising user data.

For example here, try to check this piece of code:

function Comment({ text }) {
return <div dangerouslySetInnerHTML={{ __html: text }} />;
}

In this component, we’re using dangerouslySetInnerHTML to render user-provided text, if the user writes HTML it will be rendered as-is, allowing them to execute arbitrary JavaScript (malicious included).

To prevent XSS attacks, you should avoid using dangerouslySetInnerHTML it wherever possible: React’s JSX does a great job at escaping potentially dangerous strings by default. When you need to render user-provided text, just insert it as children:

function Comment({ text }) {
return <div>{text}</div>;
}

However, what should I do if the content is coming from an external API, as string maybe? We can sanitize the content, so we know nothing dangerous will be shown. DOMPurify’s sanitize function is great to sanitize the user-provided text before inserting it into the page, removing any malicious scripts from the text, and protecting against XSS attacks.

It’s important to remember that while libraries like DOMPurify are helpful, they’re not a complete solution to XSS! Always follow the principle of least privilege when dealing with user input: don’t allow it to do more than it needs to, and sanitize it whenever possible.

import DOMPurify from 'dompurify'; // install it first obviously

function Comment({ text }) {
const sanitizedHTML = DOMPurify.sanitize(text); //now it sanitized
return <div dangerouslySetInnerHTML={{ __html: sanitizedHTML }} />;
}

2. The sneaky Insecure Direct Object References (IDOR)

We all started creating an URL like /user?id=32 for our testing application. Well, that’s exactly what IDOR is about: Insecure Direct Object References (IDOR) is a type of vulnerability in a web application that allows an attacker to bypass authorization and directly access resources in the system. This typically occurs when a developer exposes a reference to an internal implementation object, such as a file, directory, database record, or key, as a URL or form parameter (the id=32 parameter).

How does it apply on React? For example, if we don’t properly authorize the session and we allow user to manipulate the parameter in the URL, we could expose restricted data, like in this snippet:

function UserDetails({ userId }) {
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(response => response.json())
.then(data => {
// Process user data
});
}, [userId]);

// Render user details...
}

In this case, anyone can change the userId in the URL to get another user’s data, which is a serious breach of privacy.

A possible (and most common) fix for this issue, one common approach is to use tokens and server-side validation: instead of fetching the data based on the provided userId, the server should return the data for the user associated with the provided token, validating the security first.

function UserDetails() {
const [user, setUser] = useState(null);

useEffect(() => {
fetch('/api/user', {
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`
}
})
.then(response => response.json())
.then(setUser);
}, []);

// Render user details...
}

The server should then validate the token, and return the data for the user associated with that token.

3. The nerdy one: CSRF Attacks

A subtle difference but relevant is with Cross-Site Request Forgery (CSRF), where the attack tricks the victim into submitting a malicious request: it uses the identity and privileges of the victim to perform an undesired function on their behalf.

Practically, if you have an endpoint /api/posts/delete which deletes a post based on its id provided in the request body. If you’re only checking for a cookie for authentication, an attacker could create a malicious site with the following script, directly in the source code:

<script>
fetch('https://your-website.com/api/posts/delete', {
method: 'POST',
body: JSON.stringify({
id: 'some-post-id',
}),
credentials: 'include',
});
</script>

Horrible right? It could be possible to open that URL in an API software like Postman or Insomnia, keeping the same logic. The solution to protect your code in this case, could be to protect with Token, or another authorization method your endpoint from malicious usage.

4. The betrayal: component injection

Specific to React and similar libraries (and frameworks), Component Injection happens when an attacker is able to manipulate the components being rendered, which could lead to unexpected behaviors.

If we have a component that dynamically imports and renders another component based on a prop (more common than you think):

function DynamicComponent({ componentName }) {
const [Component, setComponent] = useState(null);

useEffect(() => {
import(`./${componentName}`)
.then(setComponent)
.catch(console.error);
}, [componentName]);

return Component ? <Component /> : null;
}

In this case, an attacker could potentially pass a malicious component name as a prop, and have it imported and rendered in your application. Imagine if that application is also Server Side Rendered on the first user! Manipulating that could drive to store the threatening logic on the server and serving it to all the users connected right after.

The best way to avoid this issue is to limit the number of components that can be dynamically imported and rendered, by maintaining a list of allowed components, and only importing one if its name is in the list:

const COMPONENTS = {
Home: () => import('./Home'),
Profile: () => import('./Profile'),
// ...
};

function DynamicComponent({ componentName }) {
const [Component, setComponent] = useState(null);

useEffect(() => {
const loadComponent = COMPONENTS[componentName];
if (loadComponent) {
loadComponent()
.then(setComponent)
.catch(console.error);
}
}, [componentName]);

return Component ? <Component /> : null;
}

5. The one that hit me: Open Redirects

Open redirects occur when an application incorporates user input into the target URL of a redirection without validating it: this can trick users into visiting malicious websites, leading to phishing attacks and the theft of user credentials. I’ve experienced it personally and trust me, not fun at all! 🙁

How does it look like an insecure function? Let’s take Login() one for example:

function Login() {
const [redirectTo, setRedirectTo] = useState('');

const login = async () => {
// Assume this function logs the user in...
await loginUser();

window.location.href = redirectTo;
};

// Render login form...
}

In this case, the application redirects the user to a URL provided in the redirectTo state, and it’s pretty obvious how an attacker could manipulate this URL to redirect the user to a malicious site.

To prevent this issue, you should validate the redirect URL before using it. One way to do this is by maintaining a list of allowed URLs:

const ALLOWED_REDIRECTS = ['/home', '/profile', /* ... */];

function Login() {
const [redirectTo, setRedirectTo] = useState('');

const login = async () => {
// Assume this function logs the user in...
await loginUser();

if (ALLOWED_REDIRECTS.includes(redirectTo)) {
window.location.href = redirectTo;
}
};

// Render login form...
}

Or, again, sanitize the URL input and then locked them up into a whitelist, which the combination of the two techniques is the best approach.

Drawing a conclusion…

As you can see, the list of potential security issues is far from complete, and each issue requires a unique approach to mitigate; the good news is that they are pretty known and React Dev Team is trying as much as possible to alert us of a potential threat (think about the `dangerouslySetInnerHTML` property).

Written by Muhammad Talha Waseem

Leave Comment