Exposed files are things in your web root that were never meant to be served: the .git directory, a .env file of credentials, a database dump, a phpinfo.php left from setup or a backup archive. Attackers request these paths on every site they find, because one hit yields source code or passwords. Fix it by moving them out of the web root and denying the paths in server configuration.
Why these files are the first thing an attacker requests
Most compromises do not start with a clever exploit. They start with a request for /.env or /.git/HEAD that returns 200 OK. Automated crawlers make these requests against every hostname they discover, all day, because the payoff is disproportionate: a single misconfiguration turns an anonymous outsider into someone holding your database password, your cloud API keys, your application's signing secret and a copy of your source code with every bug visible. No further vulnerability is needed. This is what a scanner means by information disclosure, and it is the category where severity is most often underestimated because "it is only a file".
The root cause is nearly always the same: the directory the web server publishes and the directory the developer works in are the same directory. Deployment copies the repository, including its metadata, into the document root. A config file is written next to the code because that is where the framework looked for it. A sysadmin runs a database dump into the current directory during an emergency and never deletes it. None of these are exotic mistakes, which is why they are so common.
What each exposed file gives away
| Path | What it contains | What an attacker does with it | Scan.now check |
|---|---|---|---|
/.git/ | Full repository: objects, refs, index, config | Downloads the whole tree and history; finds hard-coded secrets, removed files, internal hostnames and the exact code to hunt for bugs | Exposed .git repository |
/.env | Database URL and password, API keys, mail credentials, APP_KEY / SECRET_KEY | Logs into the database if it is reachable; forges signed sessions and cookies with the app secret; uses cloud keys directly | Exposed .env configuration file |
/backup.zip, /site.tar.gz, /db.sql, /wp-config.php.bak | Whole site or database snapshot | Same as above, plus every user record and password hash in the dump | Exposed backup or archive files |
/phpinfo.php, /info.php | PHP version and modules, loaded extensions, environment variables, file paths | Reads secrets passed as environment variables; picks exploits matching the exact PHP build; learns the filesystem layout | Exposed phpinfo() page |
/server-status, /nginx_status | Live request log with client IPs and URLs | Watches other users' requests, including tokens in query strings; maps internal paths | Exposed server status page |
| Directory listing | Every filename in a folder | Finds the backup you did not think to guess | Directory listing enabled |
/.DS_Store | macOS folder metadata with filenames | Same as a directory listing, from a file most people never notice uploading | Exposed .DS_Store file |
The .git directory in detail
People assume a public .git folder is harmless because directory listing is off and nobody can browse it. That is not how it is exploited. The files inside have predictable names: .git/HEAD points to a ref, the ref names a commit, the commit names a tree, the tree names blobs, and each object lives at a path computed from its hash. A tool such as git-dumper follows those pointers and reconstructs the repository one request at a time, or simply downloads .git/index, which lists every tracked filename, and fetches the packed objects. Commits that "removed the password" are still in the history. Ten minutes after the first request, the attacker has your codebase open in an editor.
The .env file in detail
Frameworks such as Laravel, Symfony, Rails (with dotenv) and many Node applications read configuration from a .env file in the project root. If the project root is the web root, https://example.com/.env serves it as plain text, because the web server has no opinion about dotfiles unless told. The consequences are immediate. Laravel's APP_KEY signs cookies and encrypts session data; with it, an attacker can forge an authenticated session for any user without touching the database. Rails' SECRET_KEY_BASE plays the same role. Database credentials work from anywhere if the database listens on a public interface, and cloud provider keys work from anywhere regardless. Rotate every value in an exposed .env, not just the ones you think were used.
Debug and status pages
phpinfo() output is often left behind by a hosting control panel or a developer checking that PHP works. It lists every environment variable the PHP process can see, which on container platforms and many managed hosts includes database URLs and API tokens injected at deploy time, plus the exact PHP version, loaded extensions and their versions, and the absolute path of the document root. Apache's mod_status page and nginx's stub_status are meant for local monitoring, and when bound to a public virtual host they stream the URLs other visitors are requesting in real time, including any tokens carried in query strings. Neither page has any business being reachable from the internet; both are one-line configuration mistakes.
How a scanner finds exposed files
A passive scanner does not crawl or guess. It holds a short list of well-known paths, requests each one, and inspects the response: the status code, the content type and a fingerprint of the body. For /.git/HEAD the expected fingerprint is the string ref: refs/heads/; for /.env it is lines shaped like KEY=value with recognisable variable names; for phpinfo.php it is the page's distinctive HTML. This matters because many sites return 200 with a friendly "not found" page for every unknown URL, and a scanner that trusted status codes alone would report every such site as leaking everything. The Scan.now website scanner makes a few dozen such requests in total, does not follow up a hit by downloading the repository or the file contents beyond the fingerprint, and never sends anything that could be mistaken for an attack. See Passive vs Active Scanning for where that boundary sits.
Active tools go further: a directory brute-forcer sends tens of thousands of requests from a wordlist and will find backup-2024-final-v2.zip where a passive scan will not. If a passive scan turns up even one exposed file, assume there are others it did not check for, and audit the web root directly on the server with find, which is faster and more complete than any remote tool.
# on the server: anything in the web root that should not be public?
find /var/www/example.com/public \( -name '.git' -o -name '.env*' -o -name '*.sql' \
-o -name '*.bak' -o -name '*.zip' -o -name '*.tar.gz' -o -name '*.log' -o -name '.DS_Store' \) -print
The structural fix: a web root that contains only public files
Deny rules are the second line of defence. The first is that the web server's document root should be a subdirectory, conventionally public/, that holds nothing but assets and a front-controller script. The repository, the .env file, vendor directories, logs and backups live one level up, where no URL can reach them. Laravel, Symfony, Rails and most modern frameworks are built this way; the exposure happens when someone points the virtual host at the project root instead of public/ because "it worked". WordPress is the notable exception, keeping wp-config.php in the web root, which is why its ecosystem relies on the deny rules below and why Scanning a WordPress Site covers this at length.
Deployment should be a build artefact, not a git clone. Export the tree with git archive or a CI build step so .git never lands on the server at all, and write secrets to the environment or a file outside the web root rather than into a .env in the project directory.
Deny rules for nginx and Apache
Add these regardless of your web root layout. They cost nothing and they catch the day a deployment script changes.
# nginx: inside the server { } block
location ~ /\.git { deny all; return 404; }
location ~ /\.(?!well-known/) { deny all; return 404; } # all dotfiles except ACME
location ~* \.(env|sql|bak|old|orig|log|ini|swp|tar|gz|zip|7z)$ { deny all; return 404; }
location ~* /(phpinfo|info)\.php$ { deny all; return 404; }
location = /server-status { allow 127.0.0.1; deny all; }
autoindex off;
# Apache 2.4: in the virtual host or a top-level .htaccess
<DirectoryMatch "/\.(?!well-known/)">
Require all denied
</DirectoryMatch>
<FilesMatch "\.(env|sql|bak|old|orig|log|ini|swp|tar|gz|zip|7z)$">
Require all denied
</FilesMatch>
Options -Indexes
<Location "/server-status">
Require local
</Location>
Returning 404 rather than 403 for denied paths is a deliberate choice: a 403 confirms the file exists. Both are acceptable; 404 gives away less. The .well-known exception matters because Let's Encrypt's HTTP challenge and security.txt (RFC 9116, reported under security.txt present) live there and must stay reachable.
/.git/HEAD, /.env and /backup.zip with curl and confirm a 404. A rule with a typo silently protects nothing, and a rule placed after a more general location block in nginx may never match.What to do if a file was exposed
- Remove or block it now, before investigating. Every minute it stays up is another crawler's copy.
- Assume it was read. Automated scanners hit new hostnames within hours of a certificate being issued; the question is not whether but how many times. Check access logs for the path to confirm.
- Rotate every secret the file contained or the repository history ever contained: database passwords, API keys, the application key, mail credentials, OAuth secrets. Rotating the application key invalidates all sessions, which is the point.
- Review the code for what an attacker now knows: hard-coded admin paths, disabled checks, internal endpoints. Anything that relied on being unknown is now known.
- Fix the deployment so it cannot recur: build artefacts, a
public/web root, deny rules, and a scan in the release pipeline.
Exposed files sit at the top of our triage order in How to Read a Security Scan Report, above TLS problems and above every header, because they are the findings that convert directly into a breach without any further skill. The website security hub places this category alongside the others an external scan covers.