Vulnerabilities
Next.js .env.local vs .env on Production Origins
October 10, 2024
Next.js .env.local vs .env on a production origin is two dotenv files that must not be public objects. Next.js loads .env.local as a local override — teams still copy it into the static export. /.env is the shared file. Both can hold DATABASE_URL, signing secrets, and server-only keys. A reachable file is the same class of miss either way. RoastMyOpsec signatures /.env and /.env.local with bounded GETs looking for KEY=value shape. Deny those names at the edge even after a pass. Do not Disallow them in robots.txt; that is a map, not a lock.
The practical difference
.env is the shared template-plus-values file people forget to gitignore. .env.local is supposed to stay on a laptop. Production CDNs see them as two paths to the same leak class.
NEXT_PUBLIC_ values belong in the client by design — that is a placement rule, not a reason to publish the whole dotenv file. Server-only names in a public .env.local are still secrets. See public JavaScript API keys vs server secrets for the browser half.
How to choose based on what you shipped
Fail CI if .env* files appear in the marketing artifact. Deny those names at the CDN on every hostname you ship, including previews. If a hit already happened, rotate what the file named. Keep examples in the private repo, not on the origin — see .env.example vs .env.
| Option | When it wins | Watch-out | Takeaway |
|---|---|---|---|
| /.env | Never on the CDN | Shared secrets in text | Roast signatures this path |
| /.env.local (Next.js) | Never on the CDN | Laptop overrides that hitchhiked | Roast signatures this path too |
| Platform env / server secrets | Runtime on hosts you control | Do not echo them into public JS | NEXT_PUBLIC_ is the only client bag |
| robots.txt Disallow | Not for dotenv | Advertises the files | Edge deny is the control |
When each dotenv file wins (off the public origin)
.env wins in private deployment config or as a checked-in example with empty values only. .env.local wins on a developer machine. Neither wins in an out/ or dist/ folder that the CDN serves.
A 200 with your SPA shell is a soft-404, not a miss. Grade the body, then deny the path anyway.
Common mistakes
The first mistake is copying .env.local into the Docker build context and then publishing that context as static files.
The second mistake is gitignoring .env.local locally and still uploading it with a manual drag-and-drop to the bucket.
The third mistake is Disallowing /.env.local in robots.txt instead of blocking it at the CDN.
What a URL roast can prove
Bounded GETs to /.env and /.env.local with a coarse KEY=value signature. No dotenv parse into a loot list, no use of leaked keys. A pass means those well-known paths did not look like real files — not that every prefix is clean. Keep denying the names.
Free audit the URL you own
RoastMyOpsec is a defensive public-surface roast: headers, cookies, sensitive paths, and more — no exploit payloads. Start with the free audit, then open the vault if the blurred findings look expensive.
Free audit nowFAQ
- Is a public .env.local a vulnerability?
- Yes — treat it like a public .env. Remove it, deny the name at the edge, and rotate secrets it may have held.
- Does RoastMyOpsec fetch .env.local?
- Yes. /.env and /.env.local are scored leftover paths today. Still deny them at the CDN after a pass so the next upload cannot land.
- Are NEXT_PUBLIC_ variables the same as a public .env.local?
- No. NEXT_PUBLIC_ is an intentional client prefix. Publishing the whole .env.local file still leaks server-only names. Keep that file off the origin.
Sources
Related guides
Vulnerabilities
.env.example vs .env on Production OriginsVulnerabilities
Exposed .git vs .env on Production WebsitesVulnerabilities
Public JavaScript API Keys vs Server Secrets