All posts
·8 min read·security · exposed files · server configuration

Seven files a scan looks for on your site that should never answer 200

Check whether your site leaks source or credentials: seven paths attackers probe everywhere, from an exposed .git directory to a stray phpinfo page, and how to close each one.

A 200 response is your server saying "here you go." For most URLs that is exactly what you want. For a handful of paths it is the worst possible answer, because the file behind them was never meant to leave the machine. An exposed .git directory hands out your source code. An exposed .env hands out your database password. Neither is rare, and neither requires a sophisticated attacker to find. Automated scanners request these same paths on every domain they encounter, yours included, dozens of times a day.

Here are the seven paths a security scan requests on your site, what each one leaks when it answers 200, and the status code it should return instead.

The exposed .git directory: /.git/HEAD

A real HEAD file is one line long, something like ref: refs/heads/main. That single line confirms a full Git repository is sitting inside your web root.

From there, the rest of the directory is reachable by predictable path. .git/index lists every tracked filename. .git/refs/ and .git/packed-refs point to commit hashes. .git/objects/ holds the compressed content. Freely available tools walk those files and rebuild the entire repository, history included, without directory listing ever being enabled. Every secret anyone committed and later "removed" is still in that history.

This usually happens for one reason: the site was deployed by running git clone or git pull directly in the document root. Git stores everything it needs in the .git folder, and the web server happily serves it.

Correct answer: 404 or 403. Better fix: deploy build output, not a checkout.

/.env

Environment files hold KEY=VALUE pairs: database credentials, API keys, SMTP passwords, application secrets, payment keys. Frameworks like Laravel, Rails, and most Node projects read them at startup.

They leak when the document root points at the project root instead of the public folder, or when a project directory is uploaded wholesale to a static host. Most servers do not recognize the .env extension, so they serve it as plain text, which is precisely the format an attacker wants.

Correct answer: 404 or 403. Better fix: keep .env one level above the web root so no URL can reach it.

/wp-config.php.bak

WordPress reads database credentials and authentication salts from wp-config.php. The PHP handler executes that file, so visitors never see its contents. A backup copy with a different extension is a different story.

Your server decides how to handle a file by its extension. wp-config.php.bak does not end in .php, so it is not executed. It is delivered as raw text with DB_NAME, DB_USER, and DB_PASSWORD visible on the first screen. Hosts create these copies during migrations. Editors create them during manual edits. Siblings to watch for: .old, .orig, .save, .swp, and files ending in a tilde.

Correct answer: 404. Better fix: never leave backup copies inside the web root.

/phpinfo.php

Developers create this file to confirm a PHP upgrade worked, then forget it exists. The phpinfo() function prints the PHP version, every loaded extension, every configuration setting, and the full contents of the server environment. On containerized hosting, that environment frequently includes database hosts and passwords passed in as variables.

The PHP manual entry for phpinfo() carries a warning about exactly this. The page is also a shopping list for attackers: it reveals the exact version to match against known vulnerabilities and the disable_functions setting that tells them which tricks will and will not work.

Correct answer: 404. Better fix: delete the file the same day you create it.

/.htaccess

Apache blocks files starting with .ht by default, so this one leaks under a specific condition: the files were written for Apache but are being served by something else. Nginx behind a migration. A static CDN origin. A container image that copied the whole folder.

The contents reveal rewrite rules pointing at admin paths you never linked publicly, IP allowlists, and the AuthUserFile directive that tells an attacker exactly where your password file lives.

Correct answer: 404 or 403.

/web.config

The IIS equivalent. IIS refuses to serve it through request filtering, so again the leak comes from the file being served by a different server or copied to a static bucket. A real one is XML and often contains connection strings with SQL Server passwords, machineKey values that allow forging authentication cookies, and custom module registrations.

Correct answer: 404 or 403.

/.DS_Store

macOS Finder writes this hidden binary file into every folder it opens. Drag a site folder onto an FTP client or sync tool and the .DS_Store files ride along. The file does not contain credentials. It contains something almost as useful: the name of every file and subfolder in that directory, including the ones you never linked. backup-2024.zip, old-site/, admin-v2/, and clients.csv all show up, and free parsers read the format in seconds.

Correct answer: 404. Better fix: exclude .DS_Store in your sync tool and your .gitignore.

Why a 200 alone does not prove a leak

Before you panic over a status code, understand the most common false alarm. Single-page apps and many hosting platforms answer 200 with the app's index.html for any unknown path. Request /.env on a typical Next.js or Vercel deployment and you get the homepage markup with a 200 attached. The status code says "found," but the body is not a .env file.

A trustworthy check does three things. It uses GET rather than HEAD so it can read the body. It rejects anything that is HTML. Then it verifies the body matches the expected format: HEAD starts with ref: or a 40-character hash, .env contains uppercase KEY= lines, web.config is XML, and phpinfo.php contains the string "PHP Version." Anything else is a fallback page, not a leak.

You can run the same test yourself:

curl -sS -o /dev/null -w "%{http_code} %{content_type}\n" https://yourdomain.com/.git/HEAD
curl -sS https://yourdomain.com/.git/HEAD | head -c 200

Output starting with ref: is a real exposure. Output starting with <!DOCTYPE html> is your app's fallback, and the status code lied.

What to return instead

Both 403 and 404 end the attack. MDN's status code reference notes that 404 reveals nothing about whether the resource exists, which is why many admins prefer it over 403, since 403 confirms there is something there to protect.

On Nginx, three location blocks cover every path above except web.config and phpinfo.php, which get their own exact matches:

location ~ /\.(?!well-known/) { return 404; }
location ~* \.(bak|old|orig|save|swp|sql|log)$ { return 404; }
location = /phpinfo.php { return 404; }
location = /web.config { return 404; }

On Apache, RedirectMatch from mod_alias does the same job and returns 404 directly:

RedirectMatch 404 /\.(?!well-known/)
RedirectMatch 404 \.(bak|old|orig|save|swp|sql|log)$
RedirectMatch 404 ^/phpinfo\.php$

The well-known exception matters. Certificate validation and several security standards rely on /.well-known/, and a blanket dotfile block that forgets it will break your next renewal.

On IIS, request filtering already hides web.config, bin, and App_Data. Add .git and .env to hiddenSegments and .bak to the denied fileExtensions list in the same section.

Blocking is the backstop. The durable fix is to stop putting these files on the server at all: deploy artifacts instead of repositories, keep secrets above the web root, delete diagnostic pages when you are done with them, and clean backup copies before upload.

Checking every site you manage

Seven curl commands on one domain take five minutes. Seven commands on every client site, every month, do not get done. The AcuityScan SSL check includes an exposed-files probe that requests each of these paths, discards HTML fallbacks, and fingerprints the body before flagging anything, so a single-page app never produces a false critical. If you are unsure which server config applies to a given site, the tech lookup reports the web server and CMS it can detect from the domain.

For the full picture, run a scan from acuityscan.com. It covers 350+ checks across 8 modules, including security headers, certificate configuration, DNS, and email authentication, and returns each finding with the exact config change that fixes it.

TL;DR

  • Seven paths should never return 200: /.git/HEAD, /.env, /wp-config.php.bak, /phpinfo.php, /.htaccess, /web.config, and /.DS_Store.
  • An exposed .git directory lets anyone rebuild your source code and commit history, secrets included.
  • .env and config backups leak credentials as plain text. phpinfo.php leaks versions, settings, and environment variables.
  • A 200 status alone is not proof. Read the body and reject HTML fallbacks before calling it a leak.
  • Return 404 or 403 for dotfiles and backup extensions, but keep /.well-known/ reachable.
  • The real fix is upstream: deploy build output, keep secrets above the web root, delete diagnostic files.

Scan your own site

See what 350+ checks find on your domain.

Free, no signup, 60 seconds. Email auth · DNS · SSL · Performance · SEO · Accessibility · Privacy · Mobile.