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);
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:
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);
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:
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:
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).
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:
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: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.
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:
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.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 itsid
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: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):
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:
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: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:
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
Recent Posts
Recent Posts
Business Rules Management with Drools: An Introduction
Reactive Programming in Java
Integrating DeepL Translation API with Java
Archives