The files you forgot you deployed

Can anyone download your .env file? (Type the URL and find out)

Your database password might be one URL away from the open internet.

Take your production domain, add /.env to the end, and load it in a browser. Thirty seconds, no tooling. If you see a 404, good. If you see a page full of DATABASE_URL=, STRIPE_SECRET_KEY=, and JWT_SECRET=, then so does everyone else who tries, and people try this constantly.

What's actually in that file

You know what's in your .env. That's the point. It's the file where every secret your app needs lives in plaintext, because it's supposed to stay on the server and never be served as a response.

A typical production .env holds the connection string to your database, with the username and password right there in the URL. It holds your payment processor keys, your mail provider token, the signing secret for your session cookies, maybe an AWS_SECRET_ACCESS_KEY. None of it is encrypted. It doesn't need to be, as long as the file is never reachable over HTTP. The whole security model is one assumption: this file is not a web route.

That assumption breaks more often than you'd think.

How it ends up downloadable

The file gets served when your document root and your project root are the same directory. On a misconfigured Apache or nginx box, on a shared host, or in a Docker image where you copied the whole project folder into the web root, a request for /.env is just a request for a static file that happens to sit next to your index.php. The server has no idea it's secret. It returns the bytes.

Frameworks try to prevent this. Laravel keeps .env outside the public folder by default. Next.js never serves it. But defaults get overridden, deploys get hand-rolled, and a single COPY . . in a Dockerfile or a wrong root directive in an nginx block undoes the protection silently. Nothing errors. The site works. The file is just quietly fetchable.

request
GET /.env HTTP/1.1\nHost: yoursite.com
response
HTTP/1.1 200 OK
Content-Type: application/octet-stream
APP_KEY=base64:7Hq...
DATABASE_URL=postgres://admin:[email protected]:5432/prod
STRIPE_SECRET_KEY=sk_live_51HxQp...
JWT_SECRET=a9f3c1...
One unauthenticated GET returns the file that was never supposed to be a response.

What a stranger does with it

They don't read it by hand. There are crawlers that do nothing all day but request /.env, /.env.local, /.env.production, and /.env.bak against every domain they can find. When one returns a 200, the contents get parsed automatically and the keys get tested in seconds.

With a working DATABASE_URL, if your database is reachable from outside (managed Postgres on a public endpoint, a Redis with a public IP), they connect directly and read every table. There's no exploit involved. They're using the credentials you handed them. With your STRIPE_SECRET_KEY, the damage looks a lot like a secret key leaked into your frontend bundle: refunds, charges, your customer list. With your JWT_SECRET, they can forge a valid session token for any user, including an admin, and your app will trust it because the signature checks out.

You won't see this happening. These are valid logins and valid API calls. The first symptom is usually data showing up somewhere it shouldn't, or your bill.

Checking it from the outside

This is the kind of thing you can confirm yourself in under a minute, and the kind of thing that's easy to miss because nobody on your team would ever request that URL on purpose.

Type https://yoursite.com/.env into a browser. Try /.env.production, /.env.local, and /.env.bak too, since people rename these and forget. A 404 or 403 is what you want. A 200 with key-value pairs is a fire.

If the file is exposed, the fix is two moves and the order matters. First, rotate every secret in it: database password, API keys, signing secrets, all of them. The file has been public for as long as it's been deployed, so treat each value as already stolen. Second, make sure the file can never be served. Move it out of the web root, or add a server rule that blocks any request for a dotfile.

# nginx: project root is the web root,
# nothing stops /.env from being served
server {
root /var/www/app;        # .env lives in here
location / { try_files $uri =404; }
}
nginx returns dotfiles like any other static file unless you tell it not to.

The good news in all of this: the file either returns content to a stranger or it doesn't, and that's a yes-or-no question you can settle from the curb. SurfaceCheckr requests /.env and its common variants the same way the crawlers do, watches what comes back, and flags any that return a real file. It never logs in and never touches your database, because the whole point is to see the deploy the way the public sees it, which is the one angle you can't get from inside your own server. Where a pentest would go digging through authenticated routes, this just answers the question on the doormat: is the file there for anyone who asks?

Adding /.env to your domain takes ten seconds. A crawler has already tried it. The only question is what it got back.

Find it before someone else does.

Paste your domain. The grade and issue count are free, and you'll see in a couple of minutes exactly what's reachable from outside.