Prevention guide

How to prevent exposed files on your website

Sensitive file exposures aren't random — they follow predictable root causes that you can eliminate before deployment. This guide covers the structural changes, server rules, and pipeline hygiene that stop .env files, .git directories, source maps, and database backups from ever reaching a publicly accessible path.

Why files get exposed: four root causes

Understanding the failure mode is the fastest path to a structural fix. The vast majority of file exposures come from one of four places:

  1. Webroot equals project root. The web server serves the entire project directory, including .env, .git/, node_modules/, and config files that should never be public. This is the most common cause on VPS deployments where the developer ran git clone directly into /var/www/.
  2. Secrets committed to version control. A .env file checked into git — even once, even briefly — lives in the commit history. If the repo is later cloned to a publicly served path, or if /.git/ becomes accessible, those secrets are recoverable.
  3. No server-side path blocking. The server has no deny rules for sensitive paths. If a file does end up in the webroot by accident, there is nothing to catch it.
  4. Source maps left enabled in production. The build tool default includes source maps. No one turned them off before the first production deploy.

Each section below addresses one of these causes directly.

Separate your project root from your webroot

The most structurally sound prevention is also the simplest: deploy only your build output, not your entire project.

A typical Node/JS project structure:

myproject/           ← project root (never serve this)
  .env               ← secrets
  .git/              ← git history
  node_modules/      ← dependencies
  src/               ← source code
  dist/              ← build output (serve only this)

On managed platforms like Netlify and Vercel, this separation is enforced by default — you specify a publish directory (dist/, out/, public/) and only that directory is served. Files outside it are never accessible regardless of what they contain.

On a VPS or self-managed server, you must create this separation deliberately:

# Wrong: serving the project root
/var/www/mysite/       ← git clone lands here; webroot is /var/www/mysite/

# Right: serving only the build output
/home/deploy/mysite/   ← git clone and build happens here
/var/www/mysite/       ← Nginx webroot; only dist/ contents are copied here

Your deploy script should copy (or rsync) only the build output to the webroot, not the project root:

npm run build
rsync -av --delete dist/ /var/www/mysite/
Symlink pattern for zero-downtime deploys. For production VPS deployments, a common pattern is to deploy to a timestamped directory (/var/www/releases/20260725/), then atomically move the current symlink. This also makes rollback instant and ensures the webroot never contains a partial deploy.

Keep secrets out of files and out of git

A .env file should exist only in two places: your local development machine (gitignored), and your hosting platform's native secret store. It should never enter version control.

Local development

Your .gitignore should include these entries from the first commit:

.env
.env.local
.env.*.local
.env.production
.env.backup
.env.old

Use git status --short before every commit to confirm no .env variants are staged. Even better, run LeakCheck on any changed file before committing — it catches secrets that slip past filename checks (hardcoded values, not just standalone .env files).

Platform secret stores (inject at build/runtime, not in files)

Vercel

Project Settings → Environment Variables. Set separate values for Production, Preview, and Development. Secrets are injected as environment variables at build time and runtime.

Netlify

Site configuration → Environment variables. Netlify injects them during the build process; they are never written to a file in the deploy output.

GitHub Actions

Repository Settings → Secrets and variables → Actions. Reference them in your workflow as ${{ secrets.MY_SECRET }}. They appear as masked strings in logs and are never stored in the repository.

VPS / Docker

Pass secrets as environment variables at runtime via docker run -e SECRET=value, docker-compose env_file pointing to a non-committed file, or a secrets manager like HashiCorp Vault or AWS Secrets Manager.

Check your history if you've ever committed .env. Platform secret stores prevent future exposure, but an .env that was committed even once survives in git history. If this has happened, follow the remediation guide — specifically the git history scrubbing steps in Step 4 — and rotate every credential that was ever in that file.

Block sensitive paths at the server level

Even with good webroot separation, a misconfiguration or accidental file copy can slip through. Server-level deny rules are your safety net — they block any request to a sensitive path regardless of how the file got there.

The paths worth blocking explicitly

A single rule covering all dot-prefixed paths handles most of them:

Nginx

# In your server {} block:
location ~ /\. {
  deny all;
  return 404;
}

Blocks /.env, /.git/, /.htaccess, and all other hidden paths. Test with nginx -t before reloading.

Apache / .htaccess

<FilesMatch "^\.">
  Require all denied
</FilesMatch>

# Also block common backup extensions
<FilesMatch "\.(sql|bak|old|backup)$">
  Require all denied
</FilesMatch>

Netlify (_redirects)

/.env 404
/.env.* 404
/.git/* 404
/*.sql 404
/*.sql.gz 404

Or use netlify.toml with [[redirects]] blocks set to status 404.

Vercel (vercel.json)

{
  "headers": [],
  "rewrites": [],
  "redirects": [
    {
      "source": "/.env",
      "destination": "/404",
      "statusCode": 404
    },
    {
      "source": "/.git/:path*",
      "destination": "/404",
      "statusCode": 404
    }
  ]
}

Cloudflare Pages (_headers)

# No native redirect rules in _headers;
# use a _redirects file (same syntax as Netlify):
/.env 404
/.git/* 404
/*.sql 404
GitHub Pages has no server-side deny rules. Plain GitHub Pages serves files directly from a repository branch and does not support custom response headers or deny rules. Prevention on GitHub Pages relies entirely on keeping sensitive files out of the repository in the first place. If you need server-level controls, proxy through Cloudflare (free tier) or migrate to Netlify or Vercel.

Disable source maps in production builds

Source maps are generated by default in most build tools to help with debugging. In production, they expose your original source code — including file paths, TypeScript types, comments, and any hardcoded values present during development — to anyone who opens DevTools.

Vite

// vite.config.js
export default {
  build: {
    sourcemap: false  // default is false
                      // check you haven't overridden it
  }
}

webpack

// webpack.config.js (production)
module.exports = {
  devtool: false,
  // or 'hidden-source-map' to generate
  // maps without referencing them in bundles
}

Create React App

# .env.production or build command:
GENERATE_SOURCEMAP=false npm run build

Next.js

// next.config.js
module.exports = {
  productionBrowserSourceMaps: false,
  // default is false; verify it hasn't been enabled
}

After rebuilding, verify the change: open the site in DevTools → Sources/Debugger. If you see webpack:// or vite:// tree entries with your original filenames, source maps are still active. Also check the Network tab for any *.js.map requests with 200 responses.

Database backup hygiene

A database dump placed in the webroot during maintenance — and never removed — is a complete data breach waiting to be discovered. The fix is a location discipline that is easy to enforce once and never think about again:

  • Never write backups to the webroot or any subdirectory of it. On a VPS with a webroot at /var/www/mysite/public/, write backups to /var/backups/mysite/ or an S3/GCS bucket.
  • Use your managed database's built-in backup feature. RDS automated backups, Supabase Point-in-Time Recovery, PlanetScale branching — these never touch your server's filesystem.
  • If you must take a manual dump, stream it directly to object storage rather than writing to disk first: pg_dump mydb | gzip | aws s3 cp - s3://my-backup-bucket/mydb-$(date +%Y%m%d).sql.gz
  • Add backup patterns to your .gitignore as a secondary safety measure: *.sql, *.sql.gz, *.bak, dump.sql.

Pre-deploy verification checklist

Run this before every launch and after every significant infrastructure change:

  • Webroot contains only build output. Confirm your CI/CD or deploy script copies only dist/ (or equivalent), not the project root.
  • No .env variants are staged or tracked. Run git ls-files | grep '\.env' — the output should be empty.
  • Source maps are disabled. Check your build config. Verify after building by inspecting the Network tab for *.js.map requests.
  • Server deny rules are in place. Confirm your Nginx/Apache config or platform _redirects / vercel.json blocks hidden paths and SQL files.
  • No backup files in the webroot. Confirm your backup destination is outside the served directory tree.
  • Run ExposureCheck on the live URL. Automated path probing and bundle scanning after every deploy catches anything the above steps missed.

Run ExposureCheck on your live site →

Frequently asked questions

What is the single most effective thing I can do to prevent .env exposure?

Keep your project root and your webroot separate. When your deploy process copies only a specific build output directory (dist/, public/, out/) to the server — not your entire project folder — files like .env, .git/, and node_modules/ never enter the served file tree. On Netlify and Vercel this is the default; on VPS deployments it requires deliberately setting your webroot to the build output directory rather than the project root.

Does .gitignore prevent a file from being served publicly?

.gitignore tells git which files to exclude from version control — it has no effect on what your web server serves. A .env file that was excluded from git can still be copied into the webroot by a deploy script, rsync command, or CI artifact step. Prevention requires both keeping sensitive files out of git and keeping them out of the webroot.

How do I prevent .git from being publicly accessible on Nginx?

Add a location block that denies all hidden paths:

location ~ /\. {
  deny all;
  return 404;
}

This single rule blocks /.git/, /.env, and all other dot-prefixed paths. Run nginx -t to test, then reload. Verify: curl -s -o /dev/null -w "%{http_code}" https://your-site.com/.git/config should return 404.

How do I disable source maps in a production build?

It depends on your build tool. Vite: set build.sourcemap: false in vite.config.js (it defaults to false in production; check you haven't overridden it). webpack: set devtool: false or 'hidden-source-map'. Create React App: set GENERATE_SOURCEMAP=false before your build command. Next.js: set productionBrowserSourceMaps: false in next.config.js. After rebuilding, verify by checking the Network tab in DevTools for any *.js.map requests.

Where should I store secrets if not in .env files in the repo?

Use your hosting platform's native environment variable storage: Vercel Project Settings → Environment Variables; Netlify Site configuration → Environment variables; GitHub Actions → Repository Settings → Secrets. For local development, keep a gitignored .env file. For teams that need to share development secrets, use a secrets manager like dotenv-vault, 1Password Secrets Automation, or Doppler — never commit the file itself.

My static site is on GitHub Pages. Can I set server deny rules?

GitHub Pages does not support custom server-side deny rules. Prevention on GitHub Pages relies entirely on keeping sensitive files out of the repository. For server-level controls, proxy through Cloudflare (free tier, supports Transform Rules) or migrate to Netlify or Vercel, both of which support deny rules via a committed config file.

How do I make sure database backup files never end up in the webroot?

Set your backup destination to a path explicitly outside the webroot — e.g. /var/backups/mysite/ on a VPS, or a cloud storage bucket. Never write backups to /var/www/ or any subdirectory of your serve path. If you use a managed database service (RDS, Supabase, PlanetScale), use their built-in backup features rather than manual SQL dumps to a local path.

Need to audit a live site for existing exposures? Use the audit guide →

Found something already exposed? Follow the step-by-step remediation guide →

Also in the Copper Bay Labs ship-safety suite

Exposed files are one part of a broader pre-launch security and compliance picture: