Almost every modern app keeps its secrets in a file called .env: the database password, the Stripe secret key, the email provider’s API key. It’s meant to stay on the server. Sometimes it doesn’t, and when it leaks, whoever finds it can act as you: read your database, issue refunds, send email in your name.
How it ends up public
- The whole folder got deployed. Some hosts and upload tools serve every file in the project folder, dotfiles included. If
.envsits in the folder the web server shares, it can be downloaded like any image. - It was committed to git. Deleting the file later doesn’t remove it from history. If the repository is ever public, or shared with the wrong person, the old keys come with it.
- A secret was given a “public” name. Frameworks copy variables that start with
NEXT_PUBLIC_,VITE_orREACT_APP_into the JavaScript every visitor downloads. That’s by design. It’s fine for a publishable key and a disaster for a secret one.
Check your own site in five minutes
- Open
https://yourdomain.com/.envin a private browser window. You want a 404 or 403 page. If you see your variables, stop reading and go to the next section. - Do the same for
/.git/config. If that loads, your whole git history may be downloadable, including old secrets. - Search your repository’s history for the file:
Any output means it was committed at some point.git log --all --oneline -- .env - Open your live site, view the page source and the main JavaScript files, and search for the start of your secret keys. Stripe secret keys begin with
sk_live_. A Supabaseservice_rolekey should never appear in the browser. The publicanonkey is meant to.
If you found something
Hiding the file is not enough. Assume anything that was public has been copied.
- Rotate every key in the file. Create new keys in each provider’s dashboard, deploy them, then revoke the old ones. This is the step that actually protects you.
- Block the file. Move
.envout of the folder your web server shares, or add a rule that refuses to serve any file whose name starts with a dot. - Keep secrets on the server. Anything with a public prefix is public. Calls that need a secret key belong in a server route or function, not in the browser.
- Stop it happening again. Add
.envto.gitignore, and turn on your git host’s secret scanning. GitHub’s push protection can block a commit that contains a known key format. - Look at the logs. Check each provider’s activity log for requests you don’t recognise from the time the key was exposed.
The quick ruleIf a value would hurt you on a billboard, it must never be in a file the web server shares or in a variable with a public prefix.