← All news
configuration4 min read

One express.static call away from serving your own .env file

By Seaworthy · August 24, 2026

A rusted padlock hanging on a chain-link gate

Photo by Meritt Thomas on Unsplash

Serving a folder of static assets in Express is one line:

app.use(express.static(path.join(__dirname, 'public')))

If your .env file, your .git directory, or a build script's .DS_Store ever end up inside that public folder, even by accident, that same one line serves them to anyone who asks for the right path.

Static file serving doesn't know which files are meant to be public

express.static (and the equivalent in most other frameworks) does exactly what it says: it serves whatever is in the directory you point it at, over HTTP, to any request that matches a file path. It has no concept of "this file looks sensitive." A .env file sitting next to index.html because a deploy script copied the whole project directory instead of just the build output is, from the static file server's perspective, just another file with a name it's happy to serve.

Dotfiles specifically have a slightly different history. Older versions of serve-static (the library express.static is built on) served dotfiles by default. Current versions default to dotfiles: 'ignore', which silently skips over them, but that default can be overridden, and plenty of code copied from older tutorials or Stack Overflow answers still explicitly sets dotfiles: 'allow' to work around some other, unrelated file-serving issue, without the person adding that line realizing what else it opens up.

Even with dotfiles: 'ignore' correctly in place, the more common way this actually happens in practice isn't a config override, it's the deploy step. If your public directory is built by copying the entire project root, or if .env, .env.local, or a leftover .git folder happen to live inside whatever directory gets served, no static-file setting saves you, because the files were never supposed to be in the served directory in the first place.

Serve a build output directory, not your project root, and verify it

// serve only the compiled output, never the project root
app.use(express.static(path.join(__dirname, 'dist')))

The dist (or build, or public) directory should be produced entirely by your build tooling, containing only the assets meant to ship, and nothing your build process didn't explicitly put there. If your deploy pipeline copies files into that directory with a shell command rather than a proper build step, double check that command's glob doesn't sweep up .env or .git along with everything else.

# after a deploy, check that nothing sensitive is reachable
curl -s -o /dev/null -w "%{http_code}\n" https://your-app.example.com/.env
curl -s -o /dev/null -w "%{http_code}\n" https://your-app.example.com/.git/config

Both should return 404. If either doesn't, something in your served directory has a file it shouldn't, and it's worth treating any secret that was in that .env as compromised and rotating it, since there's no way to know how many times it's already been requested.

For an extra layer that doesn't depend on remembering to configure this correctly in application code, most reverse proxies and CDNs can block dotfile requests at the edge:

location ~ /\. {
  deny all;
}

That rule returns a denial before the request ever reaches your Node process, which means even a misconfigured express.static call behind it can't leak a dotfile.

Why this keeps happening

"Serve the public folder" is a one-line, correct-looking instruction, and it's genuinely fine as long as the folder only ever contains what's meant to be public. The failure isn't in the static-serving code itself, which behaves exactly as documented, it's in an upstream assumption about what ends up in that directory, an assumption that's easy to get right during development (where .env sits next to dist but isn't served because you're running the dev server, not the static one) and easy to get wrong the first time a deploy script changes.

Seaworthy checks whether the directory passed to a static file server contains dotfiles, environment files, or version-control metadata, and whether dotfiles is explicitly set to allow.

This article was generated by AI and summarises publicly available sources.

From the makers of Seaworthy

Seaworthy scans your repo for the issues covered in articles like this one.

Security gaps, exposed secrets, and misconfigurations — caught before you deploy. Free to run, no account needed.

$ npx seaworthycode