In short
- A
.envfile is only exposed when it sits in a folder your server publishes: a web root that is the whole project, apublic/folder, or a build output. Rotate every value in it, then move it out. - An exposed
/.gitfolder lets anyone rebuild your source code and its full history, including secrets that were deleted in a later commit. - Production source maps publish your original frontend code. Next.js and Vite leave them off by default; Create React App generates them unless you set
GENERATE_SOURCEMAP=false. - A 200 answer is not proof. Single page apps return
index.htmlfor every unknown path, so compare the content type and the first bytes with a page that does not exist. - Blocking the path stops the next download, not the last one. Rotate the secrets first, then fix the server.
On this page
- What an exposed env file is, and how it ends up public
- What a stranger can do with each exposed file
- What an exposed .git folder gives away
- Source maps: your original code, served next to the bundle
- Backups, dumps and database files left in the web root
- How to check your own site for exposed files
- Found one? Rotate first, then block
- How to block these files on nginx, Apache, Vercel and Netlify
- The traps after the obvious fix
- What Keybrum requests on a live app
- Questions
- Sources
What an exposed env file is, and how it ends up public
A .env file holds your app's configuration as KEY=value lines: the database URL with its password, the Stripe secret key, the OpenAI key, the secret that signs session tokens. Your server reads it when it starts. A browser should never be able to download it. The file is exposed when the web server treats it like any other static file, so a request for https://your-app.com/.env returns its contents instead of a 404.
Nobody publishes a .env on purpose. It ends up public in one of three ways:
- The web root is the whole project. A server whose
rootpoints at the folder you cloned from Git serves everything in it:.env,.git/,package.json, the SQLite file from development. This is the classic setup on a VPS running nginx or Apache. - The file sits in a folder your framework publishes as is. Next.js serves every file in
public/from the root of your site, and Vite copies itspublic/folder to the root ofdistwithout touching it. A.envor a database export dropped there ships with the next deploy. - The host publishes more than you think. On Vercel, a project with no build step and the Other framework preset is served from its
publicfolder, or from the project root when there is none. The Vercel CLI uploads your folder minus a default ignore list that includes.git,.env.localand.env.*.local, but not.envor.env.production. Vercel's documentation recommends a.vercelignorefile to keep private files from being deployed and served.
What a stranger can do with each exposed file
The .env file is the famous one, but it belongs to a family. Any file that should stay on your laptop or inside your server becomes a leak once a browser can fetch it by name.
| File | What is in it | What a stranger gets |
|---|---|---|
/.env, /.env.local, /.env.production | Database URL and password, secret API keys (Stripe, OpenAI, the Supabase service role key), session or JWT secrets, mail credentials | Direct database access, your AI and payment accounts, forged sessions, and mail sent from your domain. The tool Unit 42 analyzed singled out Mailgun credentials, which can send phishing from a real domain. |
/.git/ | Your repository: every commit, branch and configuration file | Your source code and its whole history, including secrets deleted in a later commit, and sometimes a token that opens the remote repository |
*.js.map | The original source of your frontend, comments included | Readable code, your file tree, hidden routes and every API call the app makes |
backup.zip, backup.sql, dump.sql | A copy of the site or of the whole database | Every row of every table, password hashes included |
db.sqlite, data.db, dev.db | A SQLite database file | The full database, opened with any SQLite client |
serviceAccountKey.json, .npmrc, docker-compose.yml | A Google service account key, an npm token, passwords for other services | Whatever that account or token is allowed to do |
.env.bak, .env.old and site-backup.zip exist too.What an exposed .git folder gives away
The .git folder at the root of a project is the repository itself. In the words of Sysdig's researchers, it contains the complete commit history, the configuration files, the branches and the references. If your server serves that folder, anyone can download its contents and rebuild your source code. Turning off directory listing does not stop it: the layout of a Git repository uses standard names such as HEAD, config and refs/heads/main, which can be requested one by one.
Three things make it worse than a copy of today's code:
- History keeps deleted secrets. A key committed once and removed the next day is still stored in the repository's objects. If the folder was public, treat every secret that was ever committed as leaked, not only the ones in the current files.
- The config can hold a token. When a repository is cloned over HTTPS with a token in the URL,
.git/configkeeps that URL. Sysdig notes that an exposed folder can include passwords or API keys when the repository requires them. - It maps what your interface hides. Commit messages, branch names and file paths point to admin pages and internal APIs that no button in your app links to.
Attackers already collect these at scale. In 2024, Sysdig described EMERALDWHALE, an operation that scanned the internet for exposed Git configuration files between August and September of that year. It stole more than 15,000 cloud service credentials, collected credentials for over 10,000 private repositories, then cloned repositories with the stolen tokens and pulled more keys out of their code.
Source maps: your original code, served next to the bundle
Your build tool turns your source into minified JavaScript. A source map is a JSON file, usually ending in .js.map, that maps the minified code back to the original so a debugger can show readable lines. The format is standardized as ECMA-426: its sources field lists your original file paths, and its optional sourcesContent field carries the original source text itself. A comment at the end of the bundle, such as //# sourceMappingURL=app.js.map, tells the browser where to find it.
Anything in a browser bundle is public already, map or not. A source map makes it readable and puts back what minification removed: your folder structure, your comments, the admin pages your interface hides behind a role check, every endpoint the app calls, and any key someone pasted into the code. That key was in the bundle all along; the map just makes it easy to find. The guide to API keys exposed in the frontend sorts the keys that may live there from the ones that never should.
| Build tool | Production source maps by default | The setting |
|---|---|---|
| Next.js | Off | productionBrowserSourceMaps: true in next.config.js turns them on, and Next.js then serves the .map files next to the JavaScript |
| Vite | Off (build.sourcemap: false) | true writes separate .map files; 'hidden' writes them too, without the comment in the bundle |
| Create React App | On | Set GENERATE_SOURCEMAP=false to stop generating them in production builds |
Backups, dumps and database files left in the web root
The third family is files that were never meant to be deployed at all: a backup.zip made before a risky change, a dump.sql exported to move a database, a SQLite file used during development, an editor leftover such as .env.bak or config.php~. On a server whose web root is the project folder, anyone who guesses the name can download them. The web server does not know that a .sql file holds your users: it serves it like an image. A coding agent that exports a database before a migration, or creates a SQLite file for a quick local setup, does nothing wrong on your laptop. The problem starts when that file lands in the folder you deploy, so check the file list of what you ship, not only your source code.
How to check your own site for exposed files
Run these checks against your own domain only. They read, and they change nothing. The loop requests the usual paths and prints the status, the content type and the size of each answer. The last path is a file that does not exist: it is your baseline, the answer your site gives for a missing file.
SITE="https://your-app.example"
for p in /.env /.env.local /.env.production /.env.bak /.git/config \
/backup.zip /backup.sql /dump.sql /db.sqlite /data.db \
/this-file-does-not-exist.txt; do
curl -s -o /dev/null -w "%{http_code} %{content_type} %{size_download} bytes $p\n" "$SITE$p"
done- 1
A 404 or a 403 is what you want
The file is not served. Nothing more to do for that path.
- 2
A 200 with text/html and the baseline's size is not the file
Single page apps answer every unknown path with
index.htmland a 200. If the type and size match the baseline line, you are looking at your home page, not at a leak. - 3
Anything else: look at the first bytes
Run
curl -s "$SITE/.env" | head -c 300for each path that stands out.KEY=valuelines mean the env file is served.[core]means the Git config is. A file that starts withSQLite format 3is a database,CREATE TABLEorINSERT INTOis a dump, andPKis a zip archive. - 4
Look for source maps
Copy the URL of one of your JavaScript files from the page source, add
.mapto it and request it with curl. A JSON answer with"version":3and asourceslist is a source map. Before you deploy,find dist build .next/static -name "*.map" 2>/dev/nulllists the maps in your build output. - 5
Check what you deploy, not only what you serve
List the files in your build output (
dist,outorbuild) and in yourpublicfolder, and search your repository for committed env files withgit ls-files | grep -i env. If.envshows up,git rm --cached .envstops tracking it (add it to.gitignoretoo), but it stays in the history, so rotate its values. A file that is not in the deployed folder cannot be served from it.
Found one? Rotate first, then block
Blocking the path stops the next download. It does nothing about copies already taken, and the Unit 42 case shows that collecting env files happens at the scale of 110,000 domains. Work in this order:
- Rotate every secret in the file. Database password, API keys, webhook secrets, the session or JWT secret (rotating it signs everyone out, which is the point), mail credentials. A key you did not rotate still works for whoever copied it. Give each new key only the permissions the app needs.
- Look for use. Check your server's access logs for successful requests to the path, and each provider's dashboard for usage you do not recognize. What to do when an API key leaks has the steps for the providers that leak most.
- Remove the file from the deployed folder and redeploy. Then block the path at the server, as shown below, so the next mistake is caught.
- If `.git` was public, widen the search. Rotate the secrets that appear anywhere in the history, not only in the current files.
- If the file held a Supabase service role key, follow the service role key guide: that key skips every row level security policy.
How to block these files on nginx, Apache, Vercel and Netlify
The first fix is the same on every host: publish the build output, never the project folder. The rules below are a second layer, for the day someone drops a file in the wrong place.
nginx
Point root at the build output, for example /var/www/app/dist, never at the cloned repository. Then refuse dotfiles and the usual backup extensions:
# Every dotfile and dot folder, except /.well-known/ (used for certificates)
location ~ /\.(?!well-known/) {
return 404;
}
# Dumps, databases, backups, archives and source maps
location ~* \.(sql|sqlite|db|bak|old|swp|zip|map)$ {
return 404;
}nginx -t, then reload nginx.Two nginx rules decide whether this works. Regular expression locations are checked in the order they appear in the configuration, and the first match wins, so put these blocks above your other regex locations, such as a \.php$ handler. And when the longest matching prefix location has the ^~ modifier, nginx does not check regex locations at all: files under a location ^~ /static/ block are not covered by the rules above.
Apache
On Apache, point DocumentRoot at the build output as well. Then add these rules to the site's configuration, or to a .htaccess file at the web root if the server allows overrides:
# Every dotfile and dot folder, except /.well-known/
RedirectMatch 404 /\.(?!well-known/)
# Dumps, databases, backups, archives and source maps
<FilesMatch "\.(sql|sqlite|db|bak|old|swp|zip|map)$">
Require all denied
</FilesMatch>apachectl configtest.Vercel
With Next.js, files are served from public/ and from the build output, so the rule is simple: env files stay at the project root, and nothing secret goes in public/. Keep production values in the project's environment variables on Vercel rather than in a committed file. The risky case is a site with no build step: with the Other preset and no public folder, Vercel serves the project root. The CLI's default exclusions skip .env.local but not .env, so add a .vercelignore:
.env
.env.*
*.sql
*.sqlite
*.db
*.zip
*.mapNetlify
Set the publish directory to your build output (publish = "dist" in netlify.toml, or whatever folder your framework writes) so the project root is never published. As a safety net, a _redirects rule can answer 404 for a path. By default a rule does not apply when a real file exists at that path, so add ! to force it:
/.env /404.html 404!
/.env.local /404.html 404!
/.env.production /404.html 404!
/.git/* /404.html 404!The traps after the obvious fix
- Variants of the name. Blocking
/.envalone leaves.env.production,.env.bak,.env.oldand.env.save. Block the pattern, as the nginx rule above does, not one file. - A cache keeps the old copy. If a CDN sits in front of your server, purge it after removing the file, or it may keep serving the copy it stored.
- The env file was never served, but its values are in the bundle. Variables starting with
NEXT_PUBLIC_orVITE_are copied into the JavaScript at build time, so a secret in one of them is public without any.envrequest. The Next.js security guide covers public variables and server-only code. - Debug routes that print the environment. A route such as
/api/envor/api/debug, written to troubleshoot a deploy, returns the same secrets as a.envfile through your own code. Search your routes for anything that returnsprocess.env. - Deleting the file from Git does not delete it from history. If
.envwas ever committed to a repository other people can read, rotate its values.
What Keybrum requests on a live app
Keybrum runs these checks as part of its attack library, on apps whose owner proved ownership. Each one only reads:
web.env-filerequests/.env,/.env.localand/.env.production, and compares each answer with the answer for a file that does not exist, so a single page app's fallback never counts as a leak. The report lists the variable names it found, with the values masked.web.git-folderrequests/.git/configand reports the folder as served only when a real Git configuration comes back.web.exposed-filesrequests SQL dumps, SQLite files, a backup archive, server source files,.npmrc,docker-compose.yml, service account keys and.DS_Store. It reads only the first bytes of each file, confirms the type by its content, and does not store the content.web.source-mapschecks whether source maps are served next to your JavaScript.- Other attacks look for
.bakand~copies of config files, directory listings on upload and backup folders, and debug routes that return environment data.
Every env or data file found also goes through the secret detectors used on your JavaScript, so a Stripe or OpenAI key inside it is reported on its own. The fix goes to your coding agent through MCP, and Keybrum attacks again after the fix, and on every deploy with a plan. For the wider list of what to review before launch, see the vibe coding security checklist.
Questions
What happens if my .env file is exposed?
Anyone who requested it has every value in it: database credentials, API keys, signing secrets. Treat it as a leak that already happened. Rotate every secret, check logs and provider dashboards for use you do not recognize, then remove the file from the deployed folder and block the path.
How do I know if my .git folder is exposed?
Request https://your-domain/.git/config on your own site. If the answer starts with [core], the folder is served and your source code and its history can be downloaded. A 404, a 403, or your app's normal HTML page means it is not.
Are source maps a security risk in production?
They publish your original frontend code with its comments, file names and routes. They add no new secret, since a key in the map was already in the bundle, but they make a hardcoded key or a hidden admin route easy to find. Next.js and Vite keep them off in production by default; Create React App generates them unless GENERATE_SOURCEMAP=false.
Does Vercel or Netlify serve my .env file?
Not with a normal framework build, where only the build output and the public/ folder are published. The risk comes from publishing the project folder itself, or from putting the file in public/. On Vercel, a site with no build step and no public folder is served from the project root, and the CLI's default exclusions skip .env.local but not .env, so add a .vercelignore.
Is it safe to keep a .env file in my project?
Yes, on your machine and on your server, outside the folder the web server publishes. Keep it out of Git with a .gitignore entry, and keep production secrets in your host's environment settings when it offers them.
Sources
- 1.Leaked Environment Variables Allow Large-Scale Extortion Operation in Cloud Environments, Palo Alto Networks Unit 42
- 2.EMERALDWHALE: 15k cloud credentials stolen in operation targeting exposed Git config files, Sysdig Threat Research Team
- 3.gitrepository-layout, Git documentation
- 4.ECMA-426: Source map format specification, Ecma TC39
- 5.productionBrowserSourceMaps, Next.js Docs
- 6.public folder, Next.js Docs
- 7.Build options (build.sourcemap), Vite
- 8.Static asset handling (the public directory), Vite
- 9.Advanced configuration (GENERATE_SOURCEMAP), Create React App
- 10.react-scripts webpack config (GENERATE_SOURCEMAP default), Create React App on GitHub
- 11.Using .vercelignore, Vercel Docs
- 12.Build features: ignored files and folders, Vercel Docs
- 13.Configuring a build (Other preset and output directory), Vercel Docs
- 14.Rewrites and proxies (shadowing), Netlify Docs
- 15.Redirect options (404 status), Netlify Docs
- 16.Module ngx_http_core_module: location, nginx documentation
- 17.mod_alias: RedirectMatch, Apache HTTP Server documentation