August 24, 2023
In the realm of web development, securing web applications is paramount to protect user data and ensure the integrity of the system. Regular frontend security audits are essential to identify and address vulnerabilities. In this blog post, we will explore the process of performing a frontend security audit specifically for React applications. We will highlight key areas to focus on to enhance the security of your React codebase.
Begin the frontend security audit by scrutinizing the dependencies used in your React application. Remove any unused dependencies to minimize potential attack vectors. Stay updated with the latest security patches and regularly update the dependencies to mitigate known vulnerabilities.
Leverage tools like yarn audit
or npm audit
to scan your dependencies for
any known security vulnerabilities. Stay vigilant in addressing these
vulnerabilities promptly by updating to the latest secure versions or seeking
alternative libraries when necessary.
React is ‘mostly’ safe from XSS attacks. Because under the hood, it has built-in protection against XSS attacks. React automatically encodes any user input before rendering it, ensuring that user input is never executed as code. This feature makes React almost inherently secure in terms of XSS vulnerabilities.
However we can still choose to render user provided html and scripts through
dangerouslySetInnerHtml
prop or using libraries like html-react-parser
or
html-to-react
. While doing this we should be very careful to use it only when
it is absolutely necessary and make sure we are sanitizing the raw html using a
library like Dompurify
.
import DOMPurify from "dompurify";
import htmlReactParser from "html-react-parser";
import { Parser as HtmlToReactParser } from "html-to-react";
const htmlToReactParser = new HtmlToReactParser();
const UnsafeComponent = userProvidedHtml => {
return (
<>
<div dangerouslySetInnerHTML={{ __html: userProvidedHtml }} />
<div>{htmlReactParser(userProvidedHtml)}</div>
<div>{htmlToReactParser.parse(userProvidedHtml)}</div>
</>
);
};
const SafeComponent = userProvidedHtml => {
const sanitizedHtml = DOMPurify.sanitize(userProvidedHtml);
return (
<>
<div dangerouslySetInnerHTML={{ __html: sanitizedHtml }} />
<div>{htmlReactParser(sanitizedHtml)}</div>
<div>{htmlToReact(sanitizedHtml)}</div>
</>
);
};
Another XSS attack surface in React is URLs provided to anchor tags. React
doesn’t escape strings provided as html attribute props. So strings like
javascript:alert(0)
would trigger the javascript code when passed to href
prop of an anchor tag.
For example the below anchor tag would trigger a browser alert dialog when clicked.
<a href="javascript:alert(0)">Click me</a>
Similarly, the below code would also trigger a browser alert dialog.
window.location.href = "javascript:alert(0)";
We have to be careful when using links inputted by users to render links or
redirecting to them by setting window.location.href
. To prevent the code from
executing we can use backend validations to make sure the links are valid and
safe to use. For additional safety we could implement a whitelist of allowed
protocols like https
and http
and reject all other protocols on the
frontend.
const getSafeURL = url => {
const parsed = new URL(url);
if (parsed.protocol !== "https:" || paresed.protocol !== "http:") return;
return url;
};
Inspect links and redirect logic in your frontend code for potential cross-site scripting vulnerabilities. Validate user-inputted URLs to prevent unauthorized execution of malicious code. Implement measures to sanitize and escape user-generated content properly.
Examine the 3rd party scripts included in your web application and verify their necessity. Remove any unused or unnecessary scripts, reducing the attack surface and potential risks.
Ensure that all assets, such as images, fonts, and stylesheets, external scripts
are loaded over secure protocols such as https
. Loading even a single asset
over insecure protocols like http
can expose your application to potential
security risks, and attackers can exploit this to steal sensitive information.
It is a common practice to have a default header configured for all API requests
in an application. Libraries like axios
allow us to configure a default header
for all requests. This is useful when we need to attach an authentication token
to all requests. However, this can be a potential security risk if we use the
same axios instance to make requests to external APIs. This is because the
authentication token would be attached to all requests made to external APIs,
which could be exploited by attackers to gain access to sensitive information.
It is crucial to consistently encode URL paths and parameters to prevent the
execution of malicious code. Rather than manually encoding URLs each time, it is
recommended to utilize libraries specifically designed for constructing URLs
such as query-string
or qs
. By leveraging these library functions, you can
automate the encoding process and ensure that URLs are properly sanitized,
minimizing the risk of security vulnerabilities associated with unencoded or
improperly encoded data.
import qs from "qs";
const params = {
name: "John Doe",
age: 25,
};
const encodedParams = qs.stringify(params);
// encodedParams = "name=John%20Doe&age=25"
To ensure script integrity, add the integrity
attribute to the available
scripts. This attribute verifies that the script file hasn't been tampered with
and matches the original source. It provides an additional layer of protection
against malicious modifications.
<script
src="https://example.com/myscript.js"
integrity="sha384-AbCdIjK..."
></script>
Source maps are useful during development for debugging purposes, as they map the minified or transpiled code back to its original source code. However, in a production environment, making the source code easily accessible through source maps can pose a security risk. Attackers can analyze the source code to identify vulnerabilities, potentially exposing sensitive information or gaining insight into your application's inner workings. By disabling source maps, you reduce the risk of exposing your codebase to potential attackers. Source maps also provide an avenue for potential intellectual property theft. Disabling source maps in the production build helps protect your intellectual property by making it harder for unauthorized parties to reverse engineer and steal your code.
Disabling sourcemaps would also disable extensions like React Devtools, Redux Devtools, etc. which could be used by attackers to gain access to sensitive information.
Use proper state management practices in your React application. Avoid storing sensitive information in component state or global state management systems that can be accessed or modified by unauthorized users. Utilize techniques like secure context providers or server-side session management to handle sensitive data securely.
Carefully examine the usage of localStorage
, globalProps
, and cookies
in
your frontend code for sensitive data, such as user credentials, authentication
tokens, user locations, API keys and personally identifiable information.
Implement encryption and secure protocols to protect sensitive data where
required.
Review your error handling mechanisms to ensure that error messages are informative but do not divulge sensitive system details or implementation specifics. Avoid logging or displaying error messages that could potentially aid attackers in exploiting vulnerabilities, like shown below.
try {
// Some code that may throw an error
const userData = await fetchUserData(userId);
} catch (error) {
console.error("Error occurred while fetching user data:", error);
}
Instead, log errors to a secure backend service and display a generic error message to the user. This would prevent attackers from gaining access to sensitive information while still providing a good user experience.
try {
// Some code that may throw an error
const userData = await fetchUserData(userId);
// Process the fetched data
} catch (error) {
// Perform appropriate error handling, such as:
// - Providing a user-friendly error message with helpful information
// - Offering options for users to retry the operation
// - Logging the error to a secure logging service for further analysis
// - Implementing fallback behavior or graceful recovery if possible
// Log the error to a secure logging service
logError(error);
// Display a user-friendly error message
toast.error("Oops! Something went wrong. Please try again later.");
}
Leverage eslint rules to enforce best practices and prevent unsafe coding practices. Below are some recommended eslint plugins that can be used to identify and prevent potential security vulnerabilities in your application.
eslint-plugin-security Identifies potential security hotspots, such as a dangerous regular expression, square bracket notation, etc.
eslint-plugin-sonarjs SonarJS rules for ESLint to detect bugs and suspicious patterns in your code.
Content Security Policy is an HTTP header that helps mitigate various web vulnerabilities, including cross-site scripting (XSS) attacks. Define a robust CSP for your web application to specify the trusted sources from which various resources, such as scripts, stylesheets, or images, can be loaded. This restricts the execution of untrusted code and helps prevent XSS attacks.
Perform periodic reviews and updates of your frontend security measures. Stay updated with the latest security best practices, vulnerabilities, and patches. Continuously monitor security advisories for your dependencies and promptly address any reported security vulnerabilities by updating to secure versions or migrating to alternative libraries.
Conducting a frontend security audit is an essential part of safeguarding your web application from potential threats. By following the outlined steps and implementing best practices, you can significantly reduce the risk of security breaches and protect your users' data and privacy. Remember, staying proactive in maintaining the security of your frontend codebase is crucial in an ever-evolving threat landscape.
If this blog was helpful, check out our full blog archive.