Security Guides

Did you leave a secret file in a public directory? Audit your webroot

Is a token, credential file or .env in your web root? Anyone can fetch it by URL, and shared templates copy it to every site. How to audit your servers.

Published 2026-06-11 Updated 2026-10-06 15 min read
On this page

For: anyone running several sites on shared/rental hosting or a VPS, deploying into public directories (public/, public_html/, htdocs/, etc.). No attack steps — only auditing your own server and closing the holes. For protecting the .env itself, see protecting .env on shared hosting.

This site's view: where you put it IS the access control

Secrets leak more from placement accidents than from clever attacks. The webroot is "a place anyone can come fetch from by URL." Put a secret there and even the strongest server password is meaningless. This site's baseline is to keep secrets out of both the webroot and git (→ storing secrets safely). The idea is simple: put only things you don't mind anyone seeing in the public directory.

public/ = fetchable by anyone via URL

OK to place

images, CSS, JS — public assets

Never place

.env, token.json, .sql, .git, .bak

Above the app root = not served

Put here

secrets, tokens, credentials, backups. Perms 600 (owner-read only). Unreachable by URL.

Files in the public directory are fetchable by anyone via URL. Keep secrets outside it (unserved, perms 600).

Why "leftovers in the public directory" happen

The common pattern is a file placed for convenience, or one that shipped inside a boilerplate, quietly staying public.

one URL
a webroot file is fetchable by anyone
years
it sits unnoticed
every site
template leftovers spread to every site
revoke now
assume it leaked and rotate

For example, a JSON holding an access token for a third-party integration sits under public/ for a long time — and if it's from a shared template, the same file is copied to the same spot on many sites, so they all share the same problem. Fixing one site is meaningless if the others still have the file. Equally dangerous: leftover .env, database backups (.sql), a .git directory, editor backups (*.bak), and shortcuts (related: path traversal · an entire .env exposed).

Files that get left behind, and why each is dangerous

"Secret file" usually brings .env to mind, but what actually gets left behind is much broader. The testing guide from OWASP (a non-profit focused on web security) also lists old file versions, backups, and files nothing links to as things to check.

TypeTypical namesWhat is at riskWhere it belongs
Config files.env, config.php.bak, settings.jsonDatabase passwords and API keys written in plain textAbove the app root (not served)
Tokens and credentialstoken.json, credentials.json, *.pem, *.keyDirect login to an external service or another serverOutside the webroot, perms 600
Manual or editor backupslogin.php.old, index.php~, .index.php.swp, *.origWith a changed extension the file may not run as code, so its source is shown as plain textDon't create them in production; edit locally
Database dumpsbackup.sql, dump.sql.gzUsers' personal data and password hashes, all in one fileA dedicated backup location, ideally off the server
Site archivessite.zip, www.tar.gzContain .env and the full source codeDelete right after extracting
Version control data.git/Secrets from past commits may be recoverableOutside the public directory
Values embedded at build timeinside assets/index-xxxx.jsVariables starting with VITE_ or NEXT_PUBLIC_ end up in browser JavaScriptKeep secrets in server-side variables only

The last row is easy to miss. Even if you never placed a file, a build can push a secret into the public directory. Vite's official documentation states plainly that VITE_ variables must not contain sensitive information such as API keys.

Audit your own server

First, mechanically surface "any secret-looking files in my own public directory." This is inspecting your own assets, not peeking at anyone else's site.

1

Search under public directories for secret-looking files

Across your own home, surface secret candidates that slipped into public/ (token/credential JSON, .env, keys, shortcuts).
2

Assume the same file exists elsewhere — check every host and site

Sites built from the same boilerplate have the same thing in the same place; run the same check across all hosts and sites. One hit usually means there are more.
3

Decide per file whether it truly needs to be public

Sort each result into "safe to publish" vs "secret." Secrets go through the remove/revoke/move steps below.
# audit under your own public directories (inspecting your own assets)
find ~ -path '*/public/*' \( -iname '*token*.json' -o -iname '*credential*.json' \
      -o -iname '*.bak' -o -iname '*.sql' -o -iname '.env*' \) 2>/dev/null
# an exposed .git directory is dangerous too
find ~ -path '*/public/*/.git' -maxdepth 8 2>/dev/null

How to check your own environment

The find above is a starting point. Working through the five checks below catches leftovers that a filename search alone misses. Run them only against your own server and your own domain. Checks specific to .env (looking at the first bytes of the response, confirming .htaccess actually works) are covered in protecting .env on shared hosting.

1

Search for backups and archives too

Public directory names vary by host, so include public_html, htdocs and www. For example, run find ~/public_html \( -name '*~' -o -name '*.swp' -o -name '*.old' -o -name '*.orig' -o -name '*.zip' -o -name '*.tar.gz' -o -name '*.pem' -o -name '*.key' \) 2>/dev/null and confirm, file by file, that each result is really meant to be public.

2

Make sure directory listings are off

Open a directory that has no index.html (for example /uploads/) in a browser. If you see a list of file names, the web server's directory listing feature is on. A listing lets people spot leftovers without knowing their names. A 403 or 404 is what you want.

3

Search the built JavaScript for secrets

If you build a frontend, search the output folder you deploy (dist/, .next/static/ and so on). Use something like grep -rlE 'BEGIN [A-Z ]*PRIVATE KEY|secret|password' dist/, plus the prefix your service uses for its keys. If you find one, check whether the variable has a browser-facing name (VITE_, NEXT_PUBLIC_).

4

Check that secrets outside the webroot aren't readable by other users

Moving a secret out of the webroot doesn't help if loose permissions let another account on the same server read it. find ~ -maxdepth 3 \( -name '.env*' -o -name '*.pem' -o -name '*token*.json' \) -perm /o+r 2>/dev/null lists secrets readable by anyone other than the owner; fix them with chmod 600. No output means you're fine.

5

Check whether search engines have indexed anything

Search Google for site:yourdomain and look for files you don't recognise (.sql, .zip, .txt and so on). If you use Search Console, the Pages report lists the URLs it has indexed. What to do if something is still in search results is covered under common mistakes below.

What to do when you find one

Do three things as a set — remove, revoke, move. Skip any one and the fix is incomplete.

1

Remove from the webroot so the URL can't fetch it

Move the file out of the public directory or delete it, and confirm the URL now returns 404.
2

Revoke/reissue any key or token that may have leaked

Don't leave it on "probably fine." Since it was exposed, assume it leaked and refresh or revoke that token/key.
3

Keep secrets outside the webroot at perms 600

From now on, place secrets outside the public directory (e.g. above the app root) at perms 600 (owner-read only). If your config can deny serving sensitive extensions, add that too.

By server: settings that refuse to serve these files (as a backstop)

The real fix is not placing the files there. On top of that, add a deny rule on the web server so that anything placed by mistake still isn't served. What you can use depends on your setup.

SetupHow to denyDirectory listingWatch out for
Apache (VPS)<FilesMatch> + Require all denied in a <Directory> block or .htaccessTurn off with Options -IndexesIn Apache 2.4 AllowOverride defaults to None, so .htaccess is ignored unless you allow it
Nginx (VPS)deny all; inside a locationautoindex is off by defaultDoesn't read .htaccess. Regex location blocks are checked in the order written and the first match wins
Shared hostingUsually .htaccess (Apache-compatible)Via the control panel or .htaccessConfirm it works with a dummy file that holds no secrets
Static hosting / CDNWhatever is in the output folder is published as-isDepends on the serviceRely on keeping secrets out of the output folder, not on deny rules

Apache example (where .htaccess is enabled):

Options -Indexes
<FilesMatch "(^\.|~$|\.(bak|old|orig|swp|sql|zip|tar|gz|pem|key)$)">
    Require all denied
</FilesMatch>

Nginx example (inside a server block):

# Allow the path used for automatic certificate renewal (Let's Encrypt) first
location ^~ /.well-known/acme-challenge/ { }
 
# Deny files and directories starting with a dot (.env, .git, ...)
location ~ /\. { deny all; }
 
# Deny backups and archives
location ~* (~|\.(bak|old|orig|swp|sql|zip|tar|gz|pem|key))$ { deny all; }

In Nginx, when a request matches a location marked ^~, regex locations are not checked. That way /.well-known/acme-challenge/, used for certificate renewal, is still served despite the rule that denies paths starting with a dot. After changing the config, check the syntax with nginx -t, reload, and confirm a dummy file returns 403. If your site offers ZIP files or similar for download, remove that extension from the deny list.

The placement people fall into (dangerous)

  • a JSON token for an integration sits in public/
  • a secret file that shipped in the boilerplate stays public
  • backups (.sql/.bak) or .git left under the public dir
  • fix one site and call it "handled"

The correct placement

  • the public directory holds only publicly-shareable things
  • secrets live outside the webroot at perms 600
  • sensitive extensions are denied from being served
  • find one and audit every host and site

Make 'only public-safe things' a standing rule

Rules beat one-off fixes. Decide that the public directory holds only public-safe files, and as a rule never put secrets, backups, version-control data (.git), or config files there. When you update a boilerplate, strip the secret from the template itself — otherwise every site you mass-produce next keeps copying the same secret file.

Common mistakes and how to fix them

  • Relying on a deny rule and leaving the file in place: a server move or config rewrite can drop the rule, and the file is public from that day. Nginx doesn't read .htaccess at all. Treat the rule as a backstop and move the file out of the public directory.
  • Editing files directly on the production server: editors leave backups ending in ~ or .swp behind. The OWASP testing guide also advises against editing files in place on the web server. Edit locally and ship changes through your deploy.
  • Copying the whole project folder into the public directory: .env, .git and test data go out with it. Serve only the build output folder, and name exactly what gets copied.
  • Denying every dot path and breaking certificate renewal: Let's Encrypt places its check file under /.well-known/acme-challenge/. As in the Nginx example above, allow that path first.
  • Putting secrets in VITE_ or NEXT_PUBLIC_ variables: they are embedded in the built JavaScript for anyone to read. Use them only in server-side code, and revoke any key you have already shipped this way.
  • Deleting the file and stopping there: if a search engine indexed it, it can linger in results. Confirm the URL returns 404 or 410, then hide it temporarily (about six months) with the Removals tool in Google Search Console while recrawling clears it.

A real case: hidden personal data in a published report

The Department of Health Care Finance (DHCF), which runs Medicaid (public health coverage for people on low incomes) in Washington, D.C., said two statistical reports posted on its website from 2023 until July 2026 contained the underlying personal data as "hidden fields". The reports were built to display only aggregate figures, but the files still held recipients' Medicaid numbers, dates of birth and more. Its filing with the U.S. Department of Health and Human Services (HHS) put the number affected at 399,086. DHCF learned of the issue on July 21, 2026 and removed the reports.

This is not a case of a forgotten secret file, but the root is the same:

  • A file in a public directory can be fetched in full, not just the part shown on screen
  • Information the publisher thinks is "not visible" is the easiest to miss in a review
  • It can stay posted for years before anyone notices

A step that checks "what is actually inside this file" before publishing prevents both leftover files and hidden data. The full account is in the Washington, D.C. Medicaid data exposure.

What this site does itself

This site's baseline is to keep secrets — keys, tokens, connection strings — out of both the public directory and the code repository. Deploy artifacts contain only built public assets; secrets are held as environment variables of the runtime, in a place that isn't served. The reason: this article's incident class is the typical case where one leftover file leaks everything to anyone who requests its URL. We design placement itself as access control, and run this audit on the same cadence as the vulnerability-response inventory.

Sources

  • IPA, "20 Checkpoints for Secure Website Operation and Management" (Japanese; files that should not be public, search engine caches): ipa.go.jp
  • OWASP, "Web Security Testing Guide: Review Old Backup and Unreferenced Files for Sensitive Information" (.old, ~, .swp backups; don't edit in place on the server): wstg.owasp.org
  • Apache HTTP Server 2.4, "Upgrading to 2.4 from 2.2" (AllowOverride defaults to None) and "mod_autoindex" (Options +Indexes generates listings): httpd.apache.org, httpd.apache.org
  • nginx, "ngx_http_core_module: location" (matching order and ^~) and "ngx_http_autoindex_module" (autoindex defaults to off): nginx.org, nginx.org
  • Let's Encrypt, "Challenge Types" (HTTP-01 uses /.well-known/acme-challenge/): letsencrypt.org
  • Vite, "Env Variables and Modes" (VITE_ variables are exposed to client code): vite.dev
  • Next.js, "Environment Variables" (NEXT_PUBLIC_ variables are inlined into browser JavaScript): nextjs.org
  • Google Search Console Help, "Removals tool" (temporary removal lasts about six months): support.google.com
  • District of Columbia DHCF, "Notice of Data Incident": dhcf.dc.gov; HIPAA Journal (number reported to HHS): hipaajournal.com

FAQ

QWhy is putting files in a public directory dangerous?
A

Anything in the web public directory (webroot — public/, public_html/, etc.) can be fetched by anyone who hits its URL. A leftover token/credential JSON, .env, backup, or key file gets retrieved before anyone notices, leading to immediate harm. The public directory should hold only things that are safe to be public.

QIf I find one on a site, should I check the others?
A

Yes. If you mass-produce sites from a shared template or boilerplate, the same leftover is usually copied to all of them. Find one and audit every site and host of the same origin.

QWhat do I do when I find one?
A

1) Remove the file from the webroot so it returns 404 by URL. 2) Refresh/revoke any token or key that may have leaked (assume it leaked). 3) Going forward, keep secrets outside the webroot at perms 600 (owner-read only). Do all three as a set.

QWhich files should never be in a public web directory?
A

The usual suspects are config files such as .env, token or credential JSON files, database dumps (.sql), ZIP or tar.gz archives of the whole site, the .git directory, and backups made by hand or by an editor (.bak, .old, .orig, .swp, or a name ending in ~). The OWASP testing guide explains that if login.php is kept as login.php.old, the server may not run it as code and may return its source code as plain text instead.

QI deleted the file but it still shows up in search results. What do I do?
A

First make sure the URL returns 404 or 410, then use the Removals tool in Google Search Console to hide it from results temporarily. According to Google's help page, a temporary removal lasts about six months and is meant to be combined with a permanent fix such as deleting the file. Japan's IPA likewise advises asking search engines to remove cached copies. Revoke any key or token the file contained, whether or not it has left the search results.

QIs it OK to put secrets in frontend environment variables (VITE_ or NEXT_PUBLIC_)?
A

No. Vite embeds variables starting with VITE_, and Next.js embeds variables starting with NEXT_PUBLIC_, into the JavaScript sent to the browser at build time. That is the same as placing a file in the public directory: anyone can read it. Keep API keys and other secrets in variables that only server-side code uses.