Keeping env variables private in React App

Param Singh
The Startup
Published in
7 min readOct 20, 2019
Photo by Michael Dziedzic on Unsplash

Motivation

As a front-end developer, you must have found yourself in a situation on how and where to store my API keys like Google client id, client secret, firebase keys etc. The very naive approach would be to commit such sensitive information on github. And it’s not a surprise to come across such news wherein a developer was billed 14k USD for AWS usage. There are crazy crawlers out there which look for such api keys committed by tons of developers on these source control websites publicly. You could be the next! Hence, it’s important to understand on how to reduce the attack surface by following various approaches based on the level of criticality of the secret information.

Usage in CRA ecosystem

CRA (Create React App) bootstrapped React projects allow you to simply define environment variables by specifying them in the .env file in the root of the project. All these custom env variables should be prefixed with REACT_APP_ in order to be accessible

REACT_APP_GAPI_KEY=xxxxxxxxxxxxxx
REACT_APP_CLIENT_SECRET=xxxxxxxxxxxxx

Using them in the code base would be simply like

const URL = `https://example.com?${process.env.REACT_APP_GAPI_KEY}`;

What happens under the hood?

Let’s try to unveil the magic happening behind the scenes for this. We’ll have to navigate to node_modules/react-scripts/config/env.js for this.

Firstly, it identifies or resolves the .env files in the project from the root app directory as

paths.js resolving dotEnvFiles from the root directory

Then it uses the dotenv package to construct the process.env global object with all the properties specified in the env files. It basically iterates through all the env variables defined here and there and add properties to the process.env object if not already defined.

dotenv package parses the .env files and add props to process.env global variable

This means that the env variables defined in the bash command like below would take priority over the ones defined in .env files.
Also, note the parse utility parses the file contents and take the tokens out by splitting on the basis of = sign and do other things like trimming extra spaces, removing the double quotes around the values etc.

"start": "NODE_ENV=production react-scripts start"

After the process.env global object is prepared, the CRA script prepares the final copy out of it by stripping down the env variables which are not prefixes with the REACT_APP_ pattern and adding a few defaults like NODE_ENV and PUBLIC_URL as below

As you can see now, it’s filtering the env variables on line 3 to enforce the REACT_APP_ prefix and then reducing it to an object with two default properties viz NODE_ENV and PUBLIC_URL.

Ultimately, it returns a raw object and a stringified object for the same. Why? Stick around to the next section

Webpack Config

There are two places where the above result is being used in webpack.config.js of react-scripts as

Usage of raw and stringified variants of process.env variables

The InterpolateHtmlPlugin lets us use these environment variables in the html as

Interpolation syntax in HTML

Here, we would want the value of the REACT_APP_WEBSITE_NAME=Polka to be interpolated as it is without any stringification of the Polka value.

On the other hand, if we want to use any env variable inside our javascript code, interpolating it without double quotes would give a reference error. So, the stringified version wraps them in double quotes by running JSON.stringify(Polka) ==> "Polka", hence making it fit to be interpolated in JS code as

REACT_APP_ prefixed vars get replaced or interpolated during build time

However, if you try to access an env variables without the REACT_APP_ prefix, it’ll be undefined as per the transformation shown below.

Non REACT_APP_ prefixed vars gives undefined

Note: Another important gotcha here is to always store your .env in .gitignore so that it doesn’t make it to github or else it’ll defeat the whole purpose.

Env variables in non React project

In order to safely use the same technique discussed above in your general JS projects, we need to define the .env the same way in any path of your choice and include the dotenv-webpack plugin in the webpack config as

Webpack plugin for reading the .env file

It’ll do the same thing by populating the process.env object with added advantage of not having to prefix those variable names with anything. Also, as per it docs, dotenv-webpack will only expose environment variables that are explicitly referenced in your code to your final bundle. It works by text replacement and hence won’t expose the variables which aren’t referenced anywhere inside the codebase.

Secrecy & Security

We discussed how we can use it properly but is it really that proper? Are the sensitive API keys really concealed from the world? Unfortunately, the answer is NO. Since, all these things are happening in the build compilation step, the output bundle has to be fused with the code it needs to run ultimately on the client. The embedded values of the env tokens or keys would be visible eventually in the built files.

It’s just that, you’re reducing the attack surface by at least not publishing on github directly. Also, the bundled code will be obfuscated and mangled, making it hard for the attackers to find them.

And as the CRA docs say,

WARNING: Do not store any secrets (such as private API keys) in your React app!

Environment variables are embedded into the build, meaning anyone can view them by inspecting your app’s files.

The point is analogous to a situation like at an airport, you can apply a lock on your luggage to prevent it from someone opening it and stealing your stuff but it doesn’t prevent against the luggage from being stolen at all!

And here, in your front-end apps, you shouldn’t really store very critical secrets because client is never safe and sending such information to all the clients just increases the risk of them being compromised by hackers.

Solutions

  • Runtime Population: One solution could be hydrating these env variables on the global object (window) from the server side as
Interpolation in index.ejs to hydrate global var from server side

and pass the ENV_VARS variable while rendering this ejs template on say Koa server as

Koa server rendering index.html by passing variables
ENV_VARS get populated at runtime by the server

Once, it’s been read and declared on the window object, the script tag can be deleted in the end as

Removal of the script tag after it has been executed

Again, the variables would be accessible on window global variable and still they are vulnerable to be peeked by a hacker but it’s a little improvement on the previous approach since now, these vars aren’t embedded in the codebase, and thanks to name mangling while doing code uglification in prod environments, it’ll be difficult to locate the actual global variable name which holds all these globals viz ENV_VARS in this case.

  • Proxy Backend Server: The best solution so far is to eliminate the root cause and not store any sensitive information on the client. If the keys are so critical that you want to protect against theft, you need to create a backend server and give it the responsibility of storing the keys and making calls. For eg: In order to access, Open Weather API, instead of making the api directly from the React application, make a call to your middle node server as something like https://example-server.com/api/get-weather. The example-server then would make the actual request to the Open Weather API with the obtained API keys and giving back the result to the client, thus protecting the keys from outside world properly.

Conclusion

Choosing the right way around has a trade off of effort vs criticality of keys. It would be overkill to store api endpoints in a secretive manner. And even if someone steals it, most of the APIs take into account the app host names and redirect url while you registering the app on their developer console. For eg: you need to provide the redirect uri while registering for Google or Facebook OAuth. Then on top of that there are quota limitations on the api usage that you can set. So, all these methods when combined gives you full protection against inauthentic usage by someone else. The way we discussed in this article is just one of the way you can make it difficult for attackers to retrieve them. At least, it’s better than just making it a part of your code on github straight-away.

So, next time, when you come across storing some keys, evaluate its criticality if compromised, and choose the right approach.

Better safe than sorry. Happy Coding 🤓

Comments are welcome.

References

--

--

Param Singh
The Startup

Senior Front-end Engineer at AWS, Ex-Revolut, Flipkart. JS enthusiast. Cynophile. Environmentalist