Skip to content
StackSiren

Is my .env file public? How to check and fix it

By StackSiren · Published

Request https://yourdomain/.env and read the response body, not only the status code. If you get real KEY=value lines, treat every secret in that file as compromised: rotate them, remove the file from the deploy, and stop shipping .env with your build.

A status of 200 on a single-page app is often a false alarm (the host returns index.html for every path). A body that looks like your secrets is never a false alarm. This post walks through the check, the usual causes on Lovable, Bolt, Vercel and Replit deploys, and the rotate-then-fix order. Background: the exposed .env guide.

How a .env ends up on the public web

A .env file is only private if nothing serves it. Common ways it becomes downloadable:

  1. It was copied into the static output. Vite copies everything in public/ to the root of dist as-is. A .env (or .env.production) dropped into public/ is then available at /.env`.
  2. The host serves the whole project folder, not just dist or .next. Self-hosted nginx/Caddy setups and some DIY Docker images do this by accident, which can also expose /.git/config.
  3. The file was committed and the deploy includes the repo root. Some CI configs upload more than the build artefact. Vercel's CLI skips .git and some .env*.local names by default, but a plain .env is not automatically excluded — add paths to .vercelignore when needed.
  4. A backup or misnamed copy (env.txt, .env.bak, .env.old) sat next to the site files.

Separately, variables with a VITE_ or NEXT_PUBLIC_ prefix are baked into JavaScript even when .env itself is private. That is a different leak path: are VITE_ variables secret?.

Step 1: Check with curl (body matters)

Only against sites you own:

SITE=https://yourapp.com

# Status alone is not enough
curl -s -o /tmp/env-body -w "HTTP %{http_code}\ncontent-type: %{content_type}\n" "$SITE/.env"
head -20 /tmp/env-body
echo "---"
# Same idea for common variants
for path in /.env /.env.local /.env.production /.env.production.local /.git/config; do
  code=$(curl -s -o /tmp/probe -w "%{http_code}" "$SITE$path")
  # Rough HTML vs KEY=value detection
  if head -c 200 /tmp/probe | grep -qE '^[A-Za-z_][A-Za-z0-9_]*=' ; then
    echo "$path -> $code  LIKELY ENV CONTENTS"
  elif head -c 15 /tmp/probe | grep -qi '<!DOCTYPE\|<html'; then
    echo "$path -> $code  HTML (SPA fallback, not a leak by itself)"
  else
    echo "$path -> $code  (inspect /tmp/probe)"
  fi
done

Interpret the result:

What you see Meaning
404 / 403 / empty Fine for that path
200 + HTML homepage SPA catch-all — not a .env leak
200 + KEY=value lines Leak — rotate and fix
200 + [core] in /.git/config Git folder exposed — treat history as readable

You can also open https://yourapp.com/.env in a browser. If it downloads or shows secret-looking lines, same conclusion. Our env file checker automates the /.env and /.git/config probes on domains you own without displaying secret values.

Step 2: SPA 200 false positives

Many static hosts (Netlify, Cloudflare Pages, some Vercel SPA setups) rewrite unknown paths to index.html with status 200 so client-side routing works. That means:

  • curl -w "%{http_code}" returning 200 for /.env proves nothing by itself.
  • Always inspect Content-Type and the first bytes of the body.
  • A real .env is almost never text/html. Look for text/plain or an empty/octet type plus NAME=value lines.

If the body is your app shell, move on — then still search the JavaScript bundle for secrets, because a private .env can still have fed VITE_* values into the build.

Step 3: Fix per host (stop serving the file)

Do these in parallel with rotation (next section). The goal is: deploy only build output, never the env file.

Every Vite / static project

  1. Add to .gitignore: .env, .env.*, !.env.example.
  2. Never put secrets in public/.
  3. Commit .env.example with placeholder names only.
  4. Deploy dist (or your framework's output), not the repo root.

Vercel

  • Keep secrets in Project → Settings → Environment Variables (Production / Preview / Development as needed).
  • Prefer server-only names (no NEXT_PUBLIC_ / VITE_ prefix) for anything sensitive.
  • If a .env must exist locally, list it in .vercelignore so CLI uploads skip it. Dashboard env vars are the source of truth for deploys.
  • After a leak, rotate values in the dashboard and redeploy so old builds are no longer the live ones.

Netlify / Bolt-on-Netlify

  • Store secrets in Site configuration → Environment variables for Functions.
  • Publish directory should be the build output (for Vite, usually dist).
  • Netlify secret scanning can fail a build when it finds values from variables marked secret in the output — useful, not a substitute for keeping .env out of the publish folder.
  • Ask Bolt to keep keys under Secrets for server functions, not in client env files.

Lovable

  • Keep API keys in Lovable Secrets (server / Edge Function side). Lovable treats VITE_ values as browser-exposed build-time config.
  • If you connect GitHub and deploy elsewhere, recreate secrets on that host; don't copy a .env into public/.

Replit

  • Use Replit Secrets and read them only in server code.
  • Don't rely on a .env sitting in the workspace if the deployment serves the project tree. Point the web server at the build output and block dotfiles if you run a custom server.

Self-hosted (nginx example)

# Block dotfiles except ACME /.well-known/
location ~ /\.(?!well-known) {
  deny all;
}

Serve only the static build directory as the document root.

Step 4: Rotate every secret that was in the file

Assume anything in a public .env was copied. Work through the file (from your local copy or password manager — don't paste secrets into random websites):

  1. Provider keys (OpenAI, Anthropic, Stripe secret, Resend, Twilio, …): revoke/create in each dashboard. See leaked OpenAI API key for the first-hour pattern.
  2. Supabase: if a service_role / sb_secret_ value was present, follow My Supabase service_role key is exposed. Publishable/anon keys are public by design but still review RLS.
  3. Database URLs, JWT secrets, webhook signing secrets: rotate at the source, update server-only env vars, redeploy.
  4. Put replacements only in server-side config (host env vars, Edge Function secrets). Do not recreate a public .env on the CDN.

Also check git history if the file was ever committed:

git ls-files .env .env.*
git log --all -- .env

Rotating the keys is what makes an old commit harmless. Rewriting history with git filter-repo is optional and disruptive; do it only after rotation if the repo is or was public.

Step 5: Confirm clean

Re-run the curl checks. Confirm /.env no longer returns KEY=value content. Spot-check the live JS for secret-shaped strings with the exposed API key checker or a manual Sources search.

StackSiren's free scan probes /.env and /.git/config on sites you own and flags likely env bodies without showing the values. A paid $9 single report is available when you want a fuller pass you can keep; ongoing monitoring starts at Solo ($29/mo).

FAQ

curl says 200 — is that always a leak? No. Many SPAs return 200 + index.html for unknown paths. Open the body. Real KEY=value lines (or a readable /.git/config) mean a leak.

Is a public .env.example OK? Yes if it only has placeholder names and fake values. Never put real secrets in examples.

What if only public keys were in the file? Publishable Stripe keys and Supabase anon/publishable keys are designed to be public, but review what else was beside them. If anything secret shared the file, rotate the secrets.

Does deleting .env from the server fix a key that was already downloaded? No. Anyone who fetched it still has the values. Rotate first, then remove the file.

Could VITE_ values still be in my JS after I fix /.env? Yes. Fixing the file does not rewrite old bundles. Move secrets server-side, rebuild without VITE_ on those names, and redeploy.

How do I monitor this after launch? Re-check after every deploy. StackSiren's free scan covers the basic /.env / /.git probes on domains you control; use a $9 report or a Solo plan if you want that check as part of a wider pass.

Check your app now

Free passive scan, results in under a minute.

Run a free scan

Fix guides for this topic

Platform checklists: Lovable · Bolt · Vercel · Replit

Related posts

Sources