Env file exposed: how to check your site for .env, .git, source maps and backups

Your env file is exposed when your web server or host serves .env as a plain file: anyone who requests /.env on your domain gets your database password, API keys and signing secrets. The same mistake can expose /.git, source maps and backup files. Request those paths on your own site to check, rotate every value that leaked, then block the paths at the host.

The team behind Keybrum's attacksUpdated 14 min read

In short

  • A .env file is only exposed when it sits in a folder your server publishes: a web root that is the whole project, a public/ folder, or a build output. Rotate every value in it, then move it out.
  • An exposed /.git folder 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.html for 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

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 root points 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 its public/ folder to the root of dist without touching it. A .env or 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 public folder, or from the project root when there is none. The Vercel CLI uploads your folder minus a default ignore list that includes .git, .env.local and .env.*.local, but not .env or .env.production. Vercel's documentation recommends a .vercelignore file 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.

FileWhat is in itWhat a stranger gets
/.env, /.env.local, /.env.productionDatabase URL and password, secret API keys (Stripe, OpenAI, the Supabase service role key), session or JWT secrets, mail credentialsDirect 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 fileYour source code and its whole history, including secrets deleted in a later commit, and sometimes a token that opens the remote repository
*.js.mapThe original source of your frontend, comments includedReadable code, your file tree, hidden routes and every API call the app makes
backup.zip, backup.sql, dump.sqlA copy of the site or of the whole databaseEvery row of every table, password hashes included
db.sqlite, data.db, dev.dbA SQLite database fileThe full database, opened with any SQLite client
serviceAccountKey.json, .npmrc, docker-compose.ymlA Google service account key, an npm token, passwords for other servicesWhatever that account or token is allowed to do
Keybrum's exposed-file attacks request these paths, among others. Real names vary: .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/config keeps 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 toolProduction source maps by defaultThe setting
Next.jsOffproductionBrowserSourceMaps: true in next.config.js turns them on, and Next.js then serves the .map files next to the JavaScript
ViteOff (build.sourcemap: false)true writes separate .map files; 'hidden' writes them too, without the comment in the bundle
Create React AppOnSet GENERATE_SOURCEMAP=false to stop generating them in production builds
From each tool's documentation, checked in November 2026.

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.

bash
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
Replace the address with your own site. Nothing is downloaded to disk.
  1. 1

    A 404 or a 403 is what you want

    The file is not served. Nothing more to do for that path.

  2. 2

    A 200 with text/html and the baseline's size is not the file

    Single page apps answer every unknown path with index.html and a 200. If the type and size match the baseline line, you are looking at your home page, not at a leak.

  3. 3

    Anything else: look at the first bytes

    Run curl -s "$SITE/.env" | head -c 300 for each path that stands out. KEY=value lines mean the env file is served. [core] means the Git config is. A file that starts with SQLite format 3 is a database, CREATE TABLE or INSERT INTO is a dump, and PK is a zip archive.

  4. 4

    Look for source maps

    Copy the URL of one of your JavaScript files from the page source, add .map to it and request it with curl. A JSON answer with "version":3 and a sources list is a source map. Before you deploy, find dist build .next/static -name "*.map" 2>/dev/null lists the maps in your build output.

  5. 5

    Check what you deploy, not only what you serve

    List the files in your build output (dist, out or build) and in your public folder, and search your repository for committed env files with git ls-files | grep -i env. If .env shows up, git rm --cached .env stops tracking it (add it to .gitignore too), 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:

  1. 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.
  2. 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.
  3. 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.
  4. If `.git` was public, widen the search. Rotate the secrets that appear anywhere in the history, not only in the current files.
  5. 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:

nginx
# 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;
}
Remove an extension from the second rule if you serve such files on purpose. Test with 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:

apache
# 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>
Apache 2.4 syntax. The first rule answers 404, the second 403: both stop the download. Check the syntax with 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:

text
.env
.env.*
*.sql
*.sqlite
*.db
*.zip
*.map
A .vercelignore at the project root. It acts on the files you upload; source maps created during a build are controlled by the build settings in the table above.

Netlify

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:

text
/.env              /404.html  404!
/.env.local        /404.html  404!
/.env.production   /404.html  404!
/.git/*            /404.html  404!
Point the rules at a page that exists, such as your 404 page. For source maps and backups, the reliable fix is to keep them out of the publish directory.

The traps after the obvious fix

  • Variants of the name. Blocking /.env alone leaves .env.production, .env.bak, .env.old and .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_ or VITE_ are copied into the JavaScript at build time, so a secret in one of them is public without any .env request. The Next.js security guide covers public variables and server-only code.
  • Debug routes that print the environment. A route such as /api/env or /api/debug, written to troubleshoot a deploy, returns the same secrets as a .env file through your own code. Search your routes for anything that returns process.env.
  • Deleting the file from Git does not delete it from history. If .env was 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-file requests /.env, /.env.local and /.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-folder requests /.git/config and reports the folder as served only when a real Git configuration comes back.
  • web.exposed-files requests 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-maps checks whether source maps are served next to your JavaScript.
  • Other attacks look for .bak and ~ 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. 1.Leaked Environment Variables Allow Large-Scale Extortion Operation in Cloud Environments, Palo Alto Networks Unit 42
  2. 2.EMERALDWHALE: 15k cloud credentials stolen in operation targeting exposed Git config files, Sysdig Threat Research Team
  3. 3.gitrepository-layout, Git documentation
  4. 4.ECMA-426: Source map format specification, Ecma TC39
  5. 5.productionBrowserSourceMaps, Next.js Docs
  6. 6.public folder, Next.js Docs
  7. 7.Build options (build.sourcemap), Vite
  8. 8.Static asset handling (the public directory), Vite
  9. 9.Advanced configuration (GENERATE_SOURCEMAP), Create React App
  10. 10.react-scripts webpack config (GENERATE_SOURCEMAP default), Create React App on GitHub
  11. 11.Using .vercelignore, Vercel Docs
  12. 12.Build features: ignored files and folders, Vercel Docs
  13. 13.Configuring a build (Other preset and output directory), Vercel Docs
  14. 14.Rewrites and proxies (shadowing), Netlify Docs
  15. 15.Redirect options (404 status), Netlify Docs
  16. 16.Module ngx_http_core_module: location, nginx documentation
  17. 17.mod_alias: RedirectMatch, Apache HTTP Server documentation

Your app is public. So are its mistakes.

Check what any attacker can already see in it, free, in ten seconds. The full attack, with proof and fixes: €49 once, or €29 a month on a plan.

or continue with Google or GitHub