Is my .env file publicly accessible?
An .env file holds the configuration that varies between deploys: database connection strings, API keys, signing secrets, and the credentials for your backing services. It is meant to live outside your web root. When it ships inside a static build, or a server maps its directory straight to the internet, an .env file that is publicly accessible is one unauthenticated URL away from handing over everything inside it.
Why this matters
The OWASP Top 10 places this squarely in A05:2021, Security Misconfiguration. Two of the weaknesses mapped to that category are CWE-526, Exposure of Sensitive Information Through Environmental Variables, and CWE-260, Password in Configuration File, and one of OWASP's example scenarios is the directory-listing case where an attacker simply lists a folder and downloads what they find. A reachable configuration file is the same mistake under a different name.
The Twelve-Factor App describes the standard this breaks. It calls for strict separation of config from code, and gives a clean litmus test: whether the codebase could be made open source at any moment without compromising any credentials. A file that a stranger can download fails that test by definition. The blast radius is large because environment files tend to hold the most privileged secrets an application owns, from write-scoped database users to payment and email provider keys.
Build tooling usually does the right thing by design and keeps environment files out of the output directory, but deployments still leak them in predictable ways: a hand-written copy step, a misconfigured Docker image that copies the project root, or a static host pointed at a directory that was never meant to be public. Because the failure is configuration, not code, the only reliable answer is to test for it and to add a server-side block.
How to fix it
First confirm what is exposed, then deny the paths at the edge. The loop below asks for the three most common names and prints the status code, where a 200 is a confirmed exposure and a 403 or 404 is what you want.
for f in .env .env.local .env.production; do
code=$(curl -s -o /dev/null -w '%{http_code}' "https://example.com/$f")
echo "$f -> $code"
done
In Nginx, a regular-expression location that matches the dotfile can deny access; the access module's deny all returns 403. In Apache, a FilesMatch block that matches the leading dot and ends with Require all denied does the same job. On Cloudflare Pages there is no server configuration to edit, and the _headers file only sets response headers rather than denying requests, so the durable fix is to keep secrets out of the build output and add a Pages Function that runs in front of static files.
# Nginx
location ~ /\.env {
deny all;
}
# Apache
<FilesMatch "^\.env">
Require all denied
</FilesMatch>
// Cloudflare Pages — functions/_middleware.js
export async function onRequest(context) {
const { pathname } = new URL(context.request.url);
if (pathname.startsWith('/.env')) {
return new Response('Not found', { status: 404 });
}
return context.next();
}
How to verify the fix
Re-run the status-code loop after deploying. Every name should answer with 403 or 404 and, importantly, should not return the file body. Also check the built output directory itself, because a file that was never deployed cannot be served, regardless of server rules.
Automated check
The Auditor does not currently probe for exposed .env files, so the loop above is the check you should run yourself. Treat any 200 as a credential rotation event: block the path, then rotate every secret the file contained, because you cannot assume the file was never fetched.
Scan your app free →