RoastMyOpsec

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.

OptionWhen it winsWatch-outTakeaway
/.envNever on the CDNShared secrets in textRoast signatures this path
/.env.local (Next.js)Never on the CDNLaptop overrides that hitchhikedRoast signatures this path too
Platform env / server secretsRuntime on hosts you controlDo not echo them into public JSNEXT_PUBLIC_ is the only client bag
robots.txt DisallowNot for dotenvAdvertises the filesEdge 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 now

FAQ

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