Skip to content

Security basics

Is your .env file exposed? The twelve paths to check

Is your .env file exposed on your own web server? Twelve addresses tell you in a minute, and a hit means everything in the file is already public.

Vlad Tkachenko9 min read
Four sealed folders behind a wall, and one open folder in front of it with its lines of text visible, sitting under an amber band.

In short

  • Two different problems get called a .env file exposed. This one is the file itself sitting on your web server, downloadable by anyone who types the address.
  • A host that only serves your built front end cannot do this. A real server can, which is why seven of the eight apps we found were Replit apps.
  • Of 30,761 live apps where our check got an answer, 8 were handing out a private file. Twelve addresses tell you whether you are the ninth.

Somebody has told you your .env file is exposed, and you cannot tell from the sentence whether that is serious. The phrase covers two completely different situations. One of them is the tool working as designed. The other means a file you believed was private has been downloadable from your live site, by anyone, for as long as it has been there.

Here is the part guide after guide runs together: a value from your .env ending up inside your JavaScript is not the same event as the .env file itself being served by your web server. The first is the build doing what you asked it to do when you named the variable VITE_SOMETHING. The second is a filing mistake, and it is the one to check first, because you can check it yourself from outside in about a minute.

Think of your live site as a shop counter. Everything on the counter is there to be taken: the page, the images, the JavaScript, the logo. The back office behind it holds the things that run the shop, and a visitor has no route to it. A value compiled into your JavaScript is a line printed in the leaflet on the counter. The .env file answering at a public address is the folder from the back office, left out on the counter with everything else.

Is my .env file exposed?

Open your live site in a browser, put /.env on the end of the address, and press enter.

Three things can come back, and only one of them is a problem.

A page from your app. Most front ends answer every unknown address with their own index page, because that is how a single-page app routes. You get your homepage, or your "not found" screen. Nothing is being served at that path.

An error. A 403 or a 404 means the server was asked and declined. That is the answer you want.

Plain text, with lines in it. Something like SUPABASE_URL=https://… and SUPABASE_SERVICE_ROLE_KEY=eyJ…, in a monospaced wall with no styling. The file is being handed to anyone who asks for it, and it has been since the day it was first served.

Two different things get called an exposed .env file

One is about your bundle. The other is about your server.

What happenedWhere the value isWhat it takes to fix
You named a variable VITE_, NEXT_PUBLIC_ or EXPO_PUBLIC_Compiled into the JavaScript every visitor downloadsMove the work to a server, then rotate the key. The file was never served.
Your web server publishes your project directoryIn the file, at https://yoursite/.envMove the file out of what the server publishes, then rotate everything in it.

The first row is the common one and it has its own article: the prefix is an instruction to publish, and the build followed it. Everything below is the second row.

Left: the build copied one value into your JavaScript, because the prefix told it to. Right: the server is handing over the file, and the prefixes make no difference at all.

Why a Lovable app cannot do this and a Replit app can

Because the two hosts publish different things.

A builder that deploys a static front end hands its host a folder of built files: some HTML, some JavaScript, some images. Your .env was read during the build and is not in that folder, so there is nothing at /.env for anyone to fetch. The host could not serve the file if it wanted to.

A Replit app usually runs its own server, in its own project directory, with your files beside your code. A server told to serve its directory serves every file in it, and it has no way to know that one of them holds your keys. The same is true of anything you deployed to a box of your own.

The numbers follow the architecture. Of 30,761 live apps where this check got an answer, 8 were handing out at least one private file. Seven of the eight were Replit apps, and Replit apps were 3,025 of the 30,761. The eighth was on a domain of its own, which says the same thing a different way: somebody was running their own server.

That is the rarest finding we print, and the only one of the nine with no innocent explanation. If you want the wider reading of what Replit apps actually ship, we scanned 3,042 of them.

The twelve paths to check

Twelve addresses, in the order worth typing. Put each one after your domain.

AddressWhat it holdsIf it answers with text
/.envEvery key your app was built withTreat all of it as public and rotate
/.env.localThe same, from a local runSame
/.env.productionThe same, from your live deploySame
/.git/configYour repository's remote addressThe whole .git directory is usually readable
/.git/HEADWhich branch you are onSame, and it is the quietest of the twelve
/.aws/credentialsLong-lived Amazon keysRotate at Amazon, then check the bill
/database.sqlSchema and rowsEvery row of every table is public
/dump.sqlSameSame
/backup.sqlSameSame
/config.jsonWhatever you put in itRead it and see; tokens live here more often than people expect
/docker-compose.ymlService definitions, often with passwords in themRotate anything written into it
/.npmrcA registry tokenRevoke the token

Our own scan reads all twelve from outside and names the ones that answered. It takes about 20 seconds and needs no account: scan your app.

What a hit actually means

Everything in that file is public right now, and has been since the day it was first served.

You will not find out who read it. A builder gives you no log of a file being fetched that you can go and read, and the request looks like any other request for any other file. What you can be sure of is that somebody tried: automated crawlers walk exactly these twelve paths across the whole internet, all the time, and they do not need to know who you are to find yours.

Five of the eight apps we found were handing out one of the four that are immediately costly: .env, a .git directory, .aws/credentials, or a .sql dump. The other three were handing out config.json, docker-compose.yml or .npmrc. Those three are the ones people assume are harmless, which is exactly why tokens and database passwords end up written into them.

What this is not is a verdict on your app. An external check reads what your site hands to a stranger, and twelve paths answering with nothing is twelve paths answering with nothing. What else leaks out of a vibe-coded app is the longer list.

The one that ruins a week: a .git directory

A .git directory is not one file. It is your whole history.

Every commit is in there, including the one where you pasted a key in and the later one where you took it out. That is the part people get wrong about a .git leak: deleting a secret from the code you have today does nothing about the version where it was still there, and the version where it was still there is in the same directory your server is publishing.

The check reads /.git/config and /.git/HEAD because those two are small, their contents are unmistakable, and either one answering means the directory itself is being served. From there a reader does not need any special tool. The format is documented and ordinary software clones it.

If a key was ever in a commit, rotating it is the only thing that helps. Which to do first depends on whether the copy is already out, and that order is worth getting right.

The dump somebody left in the folder

Three of the twelve paths are database dumps: /database.sql, /dump.sql and /backup.sql.

A dump is every row of every table in one file, with the schema above it. Email addresses, hashed passwords, orders, messages, whatever your app holds. It answers at a public address for the dullest reason in this whole article: somebody did the responsible thing, took a copy of their database, and saved it in the project folder they happened to be standing in.

So the file made to protect the data became the fastest way to read all of it. Where a copy lives is as much of the decision as whether you take one. Three ways to back up a Supabase database covers the options, and Reeve Care keeps a copy of your Supabase database off your own server, verified before it counts as a backup.

How to fix it, and why the order matters

Move the file out of what your server publishes. Then rotate every secret that was in it. In that order.

Rotating first feels like the urgent half, and it is the half that wastes the work. Your new keys go into the same file, the file is still answering at the same address, and you have rotated straight back into the leak. Nothing is safer than it was ten minutes ago.

Rotate first and the new key lands in the file that is still being handed out. Move first and there is nothing left to hand out.

Where to put it instead, depending on what you are running:

  • A host with its own secrets store. Replit Secrets, a platform's environment variables, your hosting dashboard. The value is read by your server at run time and never sits in a file under the published directory.
  • Outside the served directory. If you control the server config, point it at a build output folder rather than at the project root. Then a file in the project root has no address at all.
  • Not in the repository either, which is a separate habit and a good one. It does nothing about today's problem.

That last point is worth saying plainly, because .gitignore is the answer everyone reaches for. It keeps the file out of your repository. The file on your server got there because the server is sitting in your project directory, and git has no opinion about that.

What to do right now

What to do

  • Type all twelve addresses after your own domain, or run the free scan and let it do the typing. Read what comes back rather than the status code.
  • If one answers with your own settings, move the file out of the directory your server publishes, and redeploy. That is the step that stops it.
  • Then rotate every key, password and token the file held, at each provider. Rotating before the file moves puts the new values back where the old ones were.
  • Check billing and provider logs afterwards. Rotation stops what happens next and does nothing about what already happened.
  • If a .git directory was readable, rotate anything that was ever in a commit, not only what is in the code today.

If you would rather work through your app as a list, the 10-minute security checklist covers this alongside the other things worth closing in a newly launched app.

FAQ

How do I know if my .env file is public?

Open your live site in a browser, put /.env on the end of the address, and press enter. If what comes back is a page from your app, nothing is being served at that path. If what comes back is plain text with lines like SUPABASE_URL=https://abcdefghij.supabase.co, the file is being handed to anyone who asks. Do the same for /.env.local and /.env.production, because a server that publishes one usually publishes all three.

Is a .env file in my bundle the same thing?

No, and the fix is different. A value that ends up inside your JavaScript got there because the build was told to put it there, by a variable named VITE_ or NEXT_PUBLIC_ or EXPO_PUBLIC_. The file never left your machine; the value did. That is a separate problem with its own article. This one is about the file itself being served by your web server, which means every value in it is public, prefixed or not.

Why can someone download my .git folder?

Because your server was pointed at your project directory, and .git is a directory inside it like any other. A server has no idea that one of them is your version history. If /.git/config or /.git/HEAD answers with text, the whole directory is usually readable, and that includes every commit you ever made rather than only the code you have now.

I found a backup.sql on my own site, what now?

Move it out of the directory your server publishes before you do anything else, because the file is being handed out while you read this. Then treat every password, key and personal record in it as public: a dump holds the schema and every row of every table. Then work out how the file got there, which is usually that somebody took a backup in the project folder and never moved it.

Do I rotate keys or delete the file first?

Move the file first, then rotate. Rotating while the file is still being served writes the new values into something anyone can download, so you spend the effort and end up where you started. Once nothing answers at that path, rotate every secret the file held, at each provider, and check billing and logs afterwards.

Written by

Vlad Tkachenko

Founder, Reeve

I spend my time looking at apps built with Lovable, Bolt, v0, Cursor and Replit, and at the short list of mistakes that keep turning up in them.

More about the author

Read next

All articles

Not sure where your own app stands?

Run a free scan and get a plain-language grade from A to F in about 20 seconds. No account, no card.

Scan your app free

Automated external check, not a full audit. Absence of findings is not a guarantee of safety.