Exposed .env files and leaked keys

An exposed .env file is a configuration file — the one holding your database password, your mail credentials and your API keys — that your web server hands to anyone who asks for it by name, and an exposed .git folder is the same accident with your entire source history attached. Neither requires an attacker to break anything: the file is simply sitting in a place the server publishes, and a single ordinary GET request downloads it. This article explains how that happens — document-root mistakes, static hosts that serve the repository itself, build artifacts nobody audited — what a finder does with the keys in the first hour, and why deleting the file afterwards is the smallest part of the response. To check your own site now, run the Leaked Secret Scanner and read along with your results.

How a private file becomes public: the document root

Every web server has a document root: the directory whose contents are public by definition. Anything placed inside it — deliberately or by accident — can be requested by URL, whether or not any page links to it. The classic accident is a project deployed by copying the whole working directory onto the server: the application code, which belongs there, arrives together with the .env file beside it, the .git folder inside it, the editor backup copies, the database dump someone made before the migration, and the archive of last month’s version. The server cannot tell these apart. It serves files; the private ones are files.

Framework conventions make the trap easy to fall into. A .env file is supposed to sit one level above the document root, or outside the deployed tree entirely, with the application reading it from disk — never through the web server. But development servers happily serve the project root, small deployments drag everything into public_html, and a configuration copied from a tutorial may point the document root at the project itself. Nothing visibly breaks when this happens, which is exactly the problem: the site works perfectly, the file is public, and the first person to notice is whoever requests /.env — a request that automated scanners make against every site on the internet, continuously.

Static hosts, served repositories and build artifacts

Static hosting removes the server configuration from the equation and replaces it with a simpler rule: whatever is in the publish directory is published. Deploy a repository to a static host with the output directory set to the project root and the host will faithfully serve the repository — .git included, if it was copied into the output. An exposed .git is worse than it looks: the HEAD file and configuration prove the layout, the object files contain every blob ever committed, and with ordinary Git tooling the entire history can be reconstructed — including secrets that were committed once and “removed” in a later commit, which removes them from the working tree and nowhere else.

Build artifacts create a second, quieter class of exposure. JavaScript bundlers inline whatever the code references, so an API key read from an environment variable at build time can end up as a literal string inside the shipped bundle — public by design, readable by every visitor, and never flagged by any file-permission check, because the file was supposed to be public. Source maps compound it by shipping the original source next to the bundle. None of this needs a misconfigured server; it needs only a build that was never inspected from the outside.

The third pattern is the leftover: deployment scripts that unpack an archive into the web root and leave the archive there, backup plugins that write dumps into a web-accessible folder, a config.php.bak saved by an editor — which many servers serve as plain text precisely because the extension no longer matches the PHP handler. Each leftover is a complete copy of something private, at a predictable address.

What attackers do in the first hour

Discovery is automated and constant. Scanners enumerate the well-known paths — env files, Git internals, configuration names, backup extensions — across huge address ranges, and a fresh exposure is typically found within hours, often faster. What arrives at your server is an unremarkable GET request that returns 200; there is no alarm, because from the server’s perspective nothing unusual happened. Assume, therefore, that any file that was public for a day was downloaded, and that the burden of proof runs the other way.

Triage is automated too. A downloaded .env is parsed in seconds: database credentials are tested against any reachable database port, cloud keys are validated with a harmless API call that says whether they are live, mail credentials are tried for sending, and payment keys are checked for their mode — a live-mode payment key and production database credentials are the jackpot, because each converts directly into money or data. Valid keys are used, traded or held quickly, because their owners may rotate them at any moment; the first hour is when an exposed key is worth the most, and it is spent efficiently.

Why deleting the file is not enough

Deleting the file stops new downloads. It does nothing about the copies that already exist: the finder’s copy, obviously, but also the copies you made yourself — the response cached by your CDN or a corporate proxy, the snapshot in a search engine’s or archive service’s crawl, the bundle a browser already downloaded. Until every cache is purged and expires, the “deleted” file may still be served by infrastructure you control but forgot to clear, and the version outside your control is outside it permanently.

That is why the first response to a confirmed exposure is rotation, not deletion: revoke the exposed credential and issue a new one, so every copy in the world becomes worthless at the same moment. Rotation has an order — issue the replacement, deploy it, verify the service works, then revoke the old credential — and it extends to everything the exposed material can unlock: a .git exposure means rotating every secret that ever appeared in the history, not just the current ones, because the history is the disclosure. Afterwards, read your access logs for the exposed paths: they tell you whether you were the first to know, and earlier fetches turn a configuration incident into a breach investigation with a very different checklist.

The probe list, explained — and the prevention checklist

A scanner like the Leaked Secret Scanner can only ask for files by name, so its list is the well-known set: /.env and its environment variants, /.git/HEAD and /.git/config, familiar configuration filenames, backup and archive extensions, and key-file names — plus the page’s own scripts, searched for key-shaped strings. The honest limit cuts both ways: a scanner finds only what it asks for, which is why it confirms a find by content, not by status code. A 200 that returns your homepage is a catch-all route, not a leak, and any report that counts it as one is inflating itself. The related supply-chain risk — scripts you deliberately load from others — is covered by third-party scripts and supply-chain risk.

Prevention is mostly placement and one layer of refusal. Keep secrets outside the document root and outside the repository’s publish output; make the server refuse the dangerous names even if they land there — deny rules for dotfiles, env and config extensions, archives and key material, tested by requesting them; strip .git from deployment artifacts or block it outright. In the pipeline, add secret scanning in CI — on commits, so a key is stopped before it ships, and on the built artifact, so a bundled key is caught in the output rather than by a stranger. Finally, scan yourself on a schedule and after every deployment change: the exposures in this article are all silent, and the only way to learn about yours before someone else does is to ask your own server the same questions the scanners ask.

Sources and further reading

Frequently asked questions

I deleted my exposed .env file. Am I safe now?

Not yet. Deletion stops new downloads, but copies may exist in the finder’s hands, in CDN and proxy caches, and in crawls and archives. Rotate every credential the file contained — revoke the old keys and issue new ones — purge your caches, and check the access logs for the path. Only rotation makes the copies worthless.

My site returns 200 for /.env. Is the file exposed?

Not necessarily. Many sites answer every unknown path with a 200 and their homepage or app shell — a catch-all fallback. What matters is the content: if the response body is your page, nothing is exposed; if it is key–value lines with real values, it is. A trustworthy scanner confirms by content and lists bare 200s separately as unconfirmed.

What should run in CI to stop this happening again?

Two checks at two moments: a secret scanner on the commit, which stops a key entering the repository, and a check of the built artifact and the deployed output directory, which catches what the build inlined and what the deployment copied. Add server deny rules for dotfiles and key material as the last layer, and re-run an external probe after deployment changes.