by Emil Lefkof III, Chief Technology Officer

Create React App (a.k.a CRA) by Facebook is one of the most popular ways to bootstrap and build React applications. CRA does an excellent job of shielding developers from the complexities of things such as Webpack, Babel, and ESLint. When producing a production ready build, CRA optimizes and minimizes your CSS and Javascript code to produce the smallest and most efficient bundle for deployment.

However, too often developers are content to assume that  Facebook has done all the hard work and that this is the best build that can be made. The question to ask yourself is: “Can I do more to secure my application from hackers?”.

This article is the result of my research and final decisions on how to harden my production applications’ security. I will show you how to make your CRA application more secure for your organization by implementing:

Content Security Policy (CSP)

Content Security Policy is your first line of defense against hackers and CSP Level 2 is supported in all modern browsers. I won’t go into detail on what CSP is but you can read more about that here.

One of the most common forms of malware infections by hackers is known as drive-by cryptojacking. Similar to malicious advertising exploits, the scheme involves embedding a piece of JavaScript code into a web page. After that, it performs cryptocurrency mining on user machines that visit the page. If you have the proper CSP configuration on your website it makes this type of malware impossible for hackers to inject on your website.

To enable CSP in your React application do the following steps.

Update .env

By default, Create-React-App will embed an in-line script into index.html during the production build.

This is a small chunk of webpack runtime logic which is used to load and run the application. The contents of this will be embedded in your build/index.html file by default to save an additional network request.

Create/update the .env file in your project root directory and set INLINE_RUNTIME_CHUNK environment variable to false, the script will not be embedded and will be imported as usual. More on GENERATE_SOURCEMAP later…

INLINE_RUNTIME_CHUNK=false
GENERATE_SOURCEMAP=false

Install CSP Plugin

Install the development dependencies including the CSP Webpack Plugin:

$ npm install react-app-rewired customize-cra @melloware/csp-webpack-plugin --save-dev

Install runtime dependencies for DOMPurify and Trusted Types:

$ npm install dompurify trusted-types

Update package.json to use React App Rewired so we can inject our Webpack build updates:

"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test"

Create config-overrides.js file in the project root directory next to package.json:

const { override } = require('customize-cra');
const CspHtmlWebpackPlugin = require("@melloware/csp-webpack-plugin");

const cspConfigPolicy = {
    'default-src': "'none'",
    'object-src': "'none'",
    'base-uri': "'self'",
    'connect-src': "'self'",
    'worker-src': "'self'",
    'img-src': "'self' blob: data: content:",
    'font-src': "'self'",
    'frame-src': "'self'",
    'manifest-src': "'self'",
    'style-src': ["'self'"],
    'script-src': ["'strict-dynamic'"],
    'require-trusted-types-for': ["'script'"]
};

function addCspHtmlWebpackPlugin(config) {
    if (process.env.NODE_ENV === 'production') {
        config.plugins.push(new CspHtmlWebpackPlugin(cspConfigPolicy));
        config.output.crossOriginLoading = "anonymous";
    }
    return config;
}

module.exports = {
    webpack: override(addCspHtmlWebpackPlugin),
};

Execute Build

Execute npm run build to generate a production build and you should be able to see these changes in the production index.html file. For each CSS and JS a nonce value is assigned and all in-line styles and scripts are blocked!

<meta http-equiv="Content-Security-Policy" 
      content="base-uri 'self'; 
      object-src 'none'; 
      script-src 'strict-dynamic' 'nonce-CrAICtit7djMzvPP/AOk1Q=='; 
      style-src 'self' 'nonce-hNq0/mMzZ+jWZOaWVGFguw==' 'nonce-5AlQofvVcR/v5P34fReEAw==' 'nonce-UzHYDLEeW68WnP3QweiB5A=='; 
      default-src 'none'; 
      connect-src 'self'; 
      worker-src 'self'; 
      img-src 'self' blob: data: content:; 
      font-src 'self'; 
      frame-src 'self'; 
      manifest-src 'self'; 
      require-trusted-types-for 'script'">

<link href="./assets/themes/lara-dark-indigo/theme.css" nonce="79pyUhldGFpDoALHNYfQzA==" rel="stylesheet">

Validate Policy

You can validate your policy is CSP Level 2/3 compliant using the Google CSP Evaluator.

PrimeReact

PrimeReact is one of the most popular React UI libraries and it has special handling for responsive design components. PrimeReact injects in-line CSS styles for some components such as the Datatable to handle the responsive features. In-line styles would be a CSP violation, but the CSP Webpack Plugin has special handling to allow PrimeReact to use its dynamic in-line styles without violating CSP rules.

Sanitizing HTML

Real-world applications often run into requirements where they need to render dynamic HTML code. Assigning text-based code and data to innerHTML is a common mistake in JavaScript applications. This pattern is so dangerous that React does not expose innerHTML directly but encapsulates it in a property called dangerouslySetInnerHTML. Improper use of the innerHTML can open you up to a cross-site scripting (XSS) attack.

Our design philosophy is that it should be easy to make things safe, and developers should explicitly state their intent when performing unsafe operations. The prop name dangerouslySetInnerHTML was intentionally chosen to be frightening.

BAD (XSS attack):
const value = `<img src="nonexistent.png" onerror="alert('You have been hacked!');" />`;

return (<p dangerouslySetInnerHTML={{__html: value}}></p>);

The above would execute the script in the browser to show you how a simple XSS attack would work. You should always sanitize your HTML before sending to innerHTML using a library like DOMPurify.

GOOD:
// Import DOMPurify
import DOMPurify from 'isomorphic-dompurify';

// Sanitize the HTML
return (<p dangerouslySetInnerHTML={{__html: DOMPurify.sanitize(value, {RETURN_TRUSTED_TYPE: true})}}></p>);

The code above will be sanitized and the alert script removed from the output. However thanks to the CSP WebPack plugin this code was automatically added so your entire codebase is now sanitized anywhere innerHTML is being used.

Subresource Integrity

Subresource Integrity (SRI) is a security feature that enables browsers to verify that files they fetch are delivered without unexpected manipulation.

The CSP Webpack Plugin automatically adds SHA384 integrity values to all CSS and JS. This allows the browser to verify that the script has not been tampered with and prevent “man in the middle” attacks.

When you view index.html of your production build you will see those values now have an integrity attribute as well as a CSP nonce attribute:

<script src="./static/js/main.8bde7ba0.js" defer="defer" 
        integrity="sha384-JLO8GNxYumunJQcIEFyDRXqQvvj+slHlDz5RGWL3n2nQ3fZoxx8zj2UoUCmIc4LT" 
        crossorigin="anonymous" 
        nonce="hV1jy80qaffHEIfJ2wpryg==">

<link href="./static/css/main.dd01fb9f.css" rel="stylesheet" 
      integrity="sha384-GySwniTtj+pQHy5qOa6ngGbRtMRbiebMO3kb4v0o7cfMnhPQsJP/NHXA53WNz2i9" 
      crossorigin="anonymous" 
      nonce="BTVl+seb2fGInJbnPSfhVQ==">

Excluding Source Maps

A source map is a special file that connects a minified/uglified version of an asset (CSS or JavaScript) to the original authored version.

Create-React-App by default will generate source maps for your CSS and JS files. Attackers will most often try to understand your code to hack their way through. Therefore, having a readable source code in the production build increases the attack surface.

Make a hacker go the extra mile and have to decode your uglified source code by adding GENERATE_SOURCEMAP=false to the .env file.

Uglified:

function a(b, c){return b*c/c};

Source Map:

function calculateFinancials (amount: double, total: double) {
   return amount * total / total;
}

This is a simple example…but what if your code was quite complex? It would be much more difficult for a hacker to figure out what your code is doing.

The second and smaller benefit is that not including source maps makes your production build faster. The note inside the Create-React-App source code says as much:

// Source maps are resource heavy and can cause out of memory issue for large source files.
const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false';

Conclusion

You can choose to use any of these tips or pick and choose which tips to use. I personally prefer to use all of them in my production applications. If anyone has any more security hardening tips I would be happy to update this article and keep it updated. All of the source code used in this article is available on GitHub here.

You can run npm run build then serve -s build and navigate to http://localhost:3000 to see this all in action!


About the Author

Emil Lefkof III is CTO and co-founder of KSM Technology Partners LLC. Read more about him here.

blog-date

Feb 16, 2022

blog-author

KSM Technology Partners