Blog · 7 November 2026 · 8 min read · for developers
Your deploy just published .env: exposed .git, .env, SQL dumps and backups on live sites – and a 60-second check
Nothing in a typical deploy pipeline stops a .env, a .git directory or last week's backup.sql from going live together with the site. The server happily serves them, HTTP 200, to anyone who guesses the path – and the guessing is automated. This post covers how the files get there (agent-driven deploys make one of the paths more common, not less), what attackers do with them, a 60-second check that does not trust HTTP 200, and the fixes for nginx, Apache, Cloudflare and static hosts.
How secrets end up in the web root
- Deploying the repository instead of the build.
git cloneorgit pulldirectly into the document root puts.git/next toindex.html. Every file ever committed – including the credentials someone removed in a later commit – is onegit-dumperrun away. The same happens with FTP uploads of a project folder and with “sync the whole directory” deploy actions. - The framework's public folder is the project folder. PHP projects served from the project root (instead of
public/) expose.env,composer.jsonandstorage/. A static site generator whose output directory accidentally contains the source tree does the same. - Backups and dumps made “just for a moment”.
wp-config.php.bakbefore an edit,dump.sqlexported by a migration script into the current directory,site.zipcreated by a hosting panel's backup tool insidepublic_html. Nobody deletes temporary files on a Friday. - Debug artefacts.
phpinfo.phpleft after “checking the PHP version”,wp-content/debug.logwithWP_DEBUG_LOGon in production,adminer.phpuploaded to fix one row in the database. - Agent-driven deploys. A coding agent asked to “deploy this” will do the fastest thing that works. If that is
rsync -a ./ user@host:/var/www/site/, the dotfiles go along. The agent does not know that.envis a secret unless the pipeline refuses to ship it. This is not an argument against agents; it is an argument for a post-deploy check that runs every time.
What is actually at stake
This is not a theoretical class of bug. In August 2024 Palo Alto Networks' Unit 42 described an extortion campaign built entirely on one request per domain – http://<target>/.env. The attackers “located these access keys by scanning and identifying exposed .env files hosted on unsecured web applications”, across “more than 230 million unique targets”. The result: “This campaign targeted 110,000 domains resulting in over 90,000 unique variables in the .env files.” Of those variables, Unit 42 writes, “7,000 belonged to organizations' cloud services” and 1,515 “were associated with social media platforms”; the files contained “hard-coded cloud provider access keys, software-as-a-service (SaaS) API keys and database login information”. The report stresses that this “did not result from vulnerabilities or misconfigurations in cloud providers' services” – the web servers simply served the file.
The .git variant is older and just as common. In 2015 Internetwache scanned the Alexa top 1M and “found about 9700 public accessible Git repositories” – under 1 % of sites, which still means thousands of production code bases with “all previous revisions, but sometimes also configuration files with sensitive information”. They report “more than 100 projects” whose .git/config stored remote credentials as protocol://user:password@host/repository, and “a lot of AWS/Database/SMTP/FTP credentials in some repositories”. Tooling has only improved since: git-dumper (“A tool to dump a git repository from a website”) reconstructs a working tree from an exposed .git even with directory listing off, and ends with git checkout ..
The OWASP Web Security Testing Guide has had a dedicated test for this class of problem for years: WSTG-CONF-04, “Review Old Backup and Unreferenced Files for Sensitive Information”. Its summary is a precise description of the problem: “Web servers commonly accumulate unreferenced or forgotten files: renamed old versions, editor/manual backups” (file~, file.old, .bak, archives), and “they tend to be served as plain text/source rather than executed, exposing server-side code, credentials, and internal details”. Its extension table lists .env under “Credentials and secrets” as a file that “should generally never be publicly served”, and the remediation is blunt: “Data files, log files, configuration files, etc. should be stored in directories not accessible by the web server”.
The 60-second check (do not trust HTTP 200)
A naive scanner requests /.env and reports a leak when the status is 200. That produces false positives on every site with a “soft 404” (a catch-all route that returns the home page for unknown paths) – which is most single-page apps and many CMS setups. Check the content, not the status. Each file type has a cheap signature:
| Path | Signature that confirms a real leak | Severity |
|---|---|---|
/.git/HEAD | Body starts with ref: refs/ (or a 40-hex commit id) | High – full source history |
/.git/config | Contains a [core] section | High – often remote URLs with tokens |
/.env | Lines matching ^[A-Z][A-Z0-9_]+= and no <html | High – live credentials |
/wp-config.php.bak, .old, ~, .save | Contains DB_PASSWORD or table_prefix (the server does not execute .bak, so it serves the PHP source) | High – database credentials |
/backup.sql, /dump.sql, /<host>.sql | Contains CREATE TABLE or INSERT INTO | High – whole database, password hashes, customer data |
/backup.zip, /site.zip, /public_html.zip, .tar.gz | First bytes PK\x03\x04 (zip) or \x1f\x8b (gzip) | High – everything above at once |
/phpinfo.php, /info.php | <title>phpinfo()</title> | Medium – paths, modules, environment variables |
/wp-content/debug.log, /error_log | PHP Warning / PHP Fatal error lines | Medium – file paths, plugin versions, sometimes data |
/adminer.php | Login form with name="auth[ | Medium – database login exposed to brute force |
/.DS_Store | Bytes \x00\x00\x00\x01Bud1 | Low – reveals file names in the directory |
One shell loop covers the high-severity rows. It requests at most 64 KB per path and matches the signature:
H=https://example.com
chk() { curl -s -m 8 -r 0-65535 "$H$1" | head -c 65536 | grep -a -q -E "$2" && echo "EXPOSED $1"; }
chk /.git/HEAD '^(ref: refs/|[0-9a-f]{40})'
chk /.git/config '^\[core\]'
chk /.env '^[A-Z][A-Z0-9_]{2,}='
chk /wp-config.php.bak 'DB_PASSWORD|table_prefix'
chk /backup.sql 'CREATE TABLE|INSERT INTO'
chk /dump.sql 'CREATE TABLE|INSERT INTO'
for f in backup site www public_html; do chk /$f.zip '^PK'; done
chk /phpinfo.php 'phpinfo\(\)'
chk /wp-content/debug.log 'PHP (Warning|Fatal error|Notice)'
Silence means nothing matched. Run it against the site you just deployed and against the staging host – staging is where the dump.sql usually lives.
If something is exposed
- Remove the file or block the path first (configs below), then rotate every secret it contained – database passwords, API keys, app keys, SMTP credentials, cloud access keys. Deleting a file that has been downloadable for weeks does not un-download it.
- For an exposed
.git, assume the entire history is public: secrets removed in a later commit are still in earlier ones. Rotate those too, and check whether the repository's remote URL in.git/configcontains a token. - For an exposed database dump: password hashes of your users were public. Force a password reset, and check what personal data was in the dump – in the EU this is likely a notifiable breach.
- Check access logs for the leaked paths to see whether anyone fetched them before you did.
Fixes that hold
nginx – deny dotfiles except ACME challenges, and deny common backup/dump extensions:
location ~ /\.(?!well-known/) { deny all; return 404; }
location ~* \.(sql|sql\.gz|bak|old|save|orig|log|zip|tar|tar\.gz|tgz|env)$ { deny all; return 404; }
location ~* ^/(phpinfo|info|adminer)\.php$ { return 404; }
Apache 2.4 (in the vhost or .htaccess if AllowOverride permits):
<FilesMatch "^\.|\.(sql|bak|old|save|orig|log|zip|tar|gz|tgz|env)$">
Require all denied
</FilesMatch>
RedirectMatch 404 /\.git
Cloudflare – a WAF custom rule that blocks URI Path containing /.git, /.env or ending in .sql / .bak stops the public download, but the file is still on the origin – fix the origin too.
Static hosts and framework hosting – the rule is “deploy the build output, never the project directory”. Keep secrets in the platform's environment variables, not in files under the output folder. Add a deploy step that fails if the output directory contains .env, .git or *.sql:
test -z "$(find dist -maxdepth 2 \( -name '.env*' -o -name '.git' -o -name '*.sql' -o -name '*.zip' \) -print -quit)" || { echo "secrets in build output"; exit 1; }
Backups – write them outside the document root (/var/backups/site/, object storage), never into public_html. If a hosting panel insists on the web root, add the deny rules above before the first backup.
Put it in the pipeline
Everything above works once. The leak comes back with the next “quick fix on the server”. The durable version is a post-deploy check that runs every time and fails loudly. Webskan's MCP server does this from the outside: audit_website probes the 28 paths from the table with the same content signatures (so soft-404 sites do not produce noise), reports each hit as a sec.exposed-file finding with the content type and size, and recheck_website tells you whether it is gone after the fix. The free scan_website runs the same probe in about a second. If you would rather script it yourself, the loop above is the whole idea – the point is that it runs after every deploy, not that it is ours.
Check a site now
The free Webskan quick scan runs the exposed-file probe, Lighthouse, security headers and the basic SEO checks on any public site. No sign-up; results in about 30 seconds.
Shipping sites with a coding agent? Give it the Webskan MCP server – free for 2 whole-site audits a day, Developer plan for 30.
Sources and tools
- Palo Alto Networks Unit 42, Leaked Environment Variables Allow Large-Scale Extortion Operation in Cloud Environments (15 August 2024) – 230 M targets scanned, 110,000 domains, 90,000 variables, 7,000 cloud keys; Unit 42 notes it “did not assess the validity of these credentials”
- Internetwache, Don't publicly expose .git or how we downloaded your website's sourcecode – an analysis of Alexa's 1M (28 July 2015) – about 9,700 exposed repositories, credentials in
.git/config, nginx/Apache rules - OWASP Web Security Testing Guide, WSTG-CONF-04: Review Old Backup and Unreferenced Files for Sensitive Information – threats, extension table, remediation (its Apache example still uses the 2.2
Deny from allsyntax; useRequire all deniedon 2.4) - nginx, location (regex modifiers
~/~*) and ngx_http_access_module (deny all); Apache, FilesMatch and Require all denied (“Access is denied unconditionally”) - arthaud/git-dumper – what an attacker runs against an exposed
.git; GitHub, About secret scanning – scans repositories, not your live web server, so it does not catch a deployed.env - Webskan, MCP server for developers and Audit the site your agent just shipped