The Real Security Risk of Environment Variables
Env vars aren't secure — they're just less insecure than hardcoding. The distinction matters more than most developers realize.
Every tutorial tells you the same thing: don't hardcode secrets, use environment variables. It's good advice — vastly better than committing your database password to GitHub. But somewhere along the way, "better than hardcoding" got upgraded in people's heads to "secure." It isn't. Environment variables are just the least-bad place to put a secret that has to live somewhere, and confusing the two has real consequences.
I got to watch this happen to a client's site a while back. Someone had added console.log(process.env) during a debugging session, forgotten it, and shipped. The staging logs — which the whole team could read — contained every key the app used: database connection string, third-party API keys, the works. Nobody noticed for weeks. When I pointed it out, the reaction wasn't "how do we fix this," it was surprise that the secret sitting in plaintext on a log server was a problem at all.
That gap between "it's in an env var" and "it's actually protected" is what this post is about.
The three ways env vars leak
Debug logging. The one above. console.log(process.env) during debugging, forgotten in a commit. Now your credentials are in CloudWatch or whatever log sink you use, readable by anyone with log-viewer access. I've seen production apps log the entire environment object at startup, every boot, every key, plaintext.
Client bundles. This is the Next.js/Vite footgun that catches people constantly. Prefix a variable with NEXT_PUBLIC_ or VITE_ and it gets inlined into your client-side JavaScript — which means anyone with DevTools can read it. A client bundle is public by definition; there is no "private" version of it. I've found API keys with write access embedded in production frontends from developers who assumed "environment variable = safe" and never asked where the variable actually ended up.
Compromised dependencies. The one that genuinely keeps me up at night. Any package in your node_modules that runs code can call process.env and read every secret your app holds. One malicious dependency — even transitive, even "harmless" — and all of it is gone. This isn't hypothetical; supply-chain attacks that harvest environment variables are documented and recurring.
Why a leak here is worse than other leaks
Think about what actually lives in env vars: database connection strings with credentials baked in, third-party API keys, JWT signing secrets, cloud access tokens, SMTP passwords. These aren't one bad endpoint — they're the master keys. If one leaks, an attacker doesn't need to find another vulnerability or move laterally. They just log in as you. Everything else you've secured becomes irrelevant.
What "actually secure" looks like
The core problem is architectural: process.env is globally readable by every process in the runtime. Real secrets management moves away from that:
- Secret managers (Vault, AWS Secrets Manager, GCP Secret Manager): fetched at runtime, on demand, never parked in the environment.
- Short-lived credentials: tokens that expire on their own, so a leak has a shelf life measured in minutes, not years.
- Scoped access: each service gets only the secrets it specifically needs, not the whole bag.
- Isolated delivery: secrets handed to the process that needs them, not readable by every dependency and every log line in the tree.
The principle underneath all of it: secrets should be retrievable by the thing that needs them, not globally readable by everything.
I'm not saying ditch env vars tomorrow. For side projects and prototypes they're fine, and I use them constantly. But when an app touches real user data or real money, treat env vars as what they are: a convenient holding area with a false sense of security attached — and build the actual protection on top of it.
Enjoyed this read?
Share it with your network.