Keeping env variables private in React App
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
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.
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
The InterpolateHtmlPlugin
lets us use these environment variables in the html as
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
However, if you try to access an env variables without the REACT_APP_
prefix, it’ll be undefined as per the transformation shown below.
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
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
and pass the ENV_VARS variable while rendering this ejs template on say Koa server as
Once, it’s been read and declared on the window object, the script tag can be deleted in the end as
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
. Theexample-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.