August 2, 2026·10 min read Deep dive
Keys and source code files leaking out of a server exposed to the internet, with a warning sign beside it

Exposed .git and a forgotten .env: how your source code leaks out

This is not exotic, and it is not a sophisticated attack. The site works, nothing looks broken, and at your-site.com/.git/config the browser calmly shows you plain text — and with it, the project's source code and its entire edit history walk out the door. Let us look at where this comes from, what makes it genuinely dangerous and how to close it in a single evening.

What the .git directory is and why it ended up on your server

.git is the service directory of the Git version control system: it holds the project's whole history, meaning every version of every file a developer has ever saved. The site itself does not need this directory to run — it exists only for the people who write the code.

It has no business being on a production server, and yet it keeps turning up there. The reasons are the same dull handful:

Notice that not one of these points involves malice or a rare mistake. This is the ordinary life of an ordinary site, which is why the finding shows up at companies of every size.

Why this is worse than “so they will see the source code”

An owner's first reaction is usually calm: “So what, there is nothing secret in there, it is a plain PHP site.” The trouble is that an exposed .git is not “looking at a couple of files”. It is access to the project's archive, from which the working source code can be reconstructed in full — and with it, the entire history of changes.

What that means in practice:

The key point: deleting a password from a file does not delete it from history

Git works like this: every save (a commit) records the state of the files at that moment and keeps it forever. A new commit does not overwrite the old one — it is added alongside. History is not a change log, it is a set of complete snapshots.

Hence the familiar story, which has played out in almost every project:

  1. In a hurry, a developer wrote the database password straight into a configuration file and committed the change.
  2. A week later he thought better of it, moved the password into environment variables and deleted it from the file. New commit.
  3. The current version of the code has no password in it. Everyone relaxes.

But the week-old commit has not gone anywhere — and the password sits in it in plain text. Anyone holding a copy of the repository reads not only the latest version of a file, but every previous one. Which means every key, password and token that made it into a commit even once stays available forever, even if it was pulled out of the working code the same day.

A simple analogy: you filed a contract with the amount written in it, then a week later brought in a corrected version without the amount. Nobody pulled the first copy out of the archive. Git behaves exactly the same way — the new version is appended, the old one stays.

That is why an exposed .git is dangerous even on a brochure site with “nothing to steal”: what matters is not the current code, but what once lay in that code and was forgotten.

What usually lies next to it

An exposed .git is rarely the only finding: all these files share one cause — the working folder was uploaded to the server whole. It is worth checking the entire set at once.

FileWhat is in itWhat it costs you
.env database password, payment gateway keys, mailing tokens, session secret the worst case: credentials are handed over as a ready-made list, with no code reconstruction needed at all
dump.sql, backup.zip, site.tar.gz a database export or a site archive, left in the root after a migration customer database, orders, user password hashes — downloaded in a single request
.svn the same as .git, but from the Subversion system turns up on older sites and gets probed by attackers just as automatically
composer.lock, package.json a full list of libraries with exact versions not a vulnerability in itself, but it spares the attacker the guesswork: what is outdated is right there
.idea/, .vscode/, .DS_Store editor settings, sometimes server connection parameters; .DS_Store — a listing of the files in a folder reveals the project's internal structure and service paths
phpinfo.php, test.php, info.php the full PHP configuration: versions, paths, modules, sometimes environment variables left “for five minutes” during debugging and forgotten for years

Attackers do not guess these addresses — they run through them automatically, from a list, against everyone in turn. So “our site is small, who would bother” does not apply here: nobody picked your site personally.

How to check your own site in a minute

You need no tools, a browser is enough. Open these one by one, substituting your own domain:

  1. your-site.com/.git/config
  2. your-site.com/.env
  3. your-site.com/dump.sql and your-site.com/backup.zip
  4. your-site.com/phpinfo.php

What the answer means:

What you sawWhat it means
The text of the file, or an offer to download it Bad. The file is available to the whole internet right now. Go to the “How to close it” section — and if this is .git or .env, then afterwards be sure to read the section on reissuing secrets.
403 Forbidden Access is closed — already not bad, but a reason to look at the setup. A 403 confirms that the file exists, and it often means that only this one file is closed rather than the whole directory. Check /.git/HEAD and /.git/index while you are at it: it happens that one thing was closed and everything next to it stayed open.
404 Not Found, or the site's ordinary error page Fine. Either the file is not there, or the server is set up to answer 404 to such requests — and that is exactly the behaviour you want.
Check only your own site this way. Opening these addresses on someone else's resource — let alone pulling the repository off it — is no longer research, it is unauthorised access to computer information, with everything that follows. If you spot a problem at someone you know, write to them instead of “checking in more detail”.

Files left open like this are exactly what any external site check finds, free ones included. We have written separately about where the limits of automated scanners actually lie: what free vulnerability scanners find and what they will not show you.

How to close it

nginx

Add a rule to the server { ... } block that forbids serving anything starting with a dot:

location ~ /\.(?!well-known) {
    deny all;
}

It closes .git, .env, .svn, .idea and any other hidden files and directories in one go.

The .well-known exception must stay. The /.well-known/ directory also starts with a dot, but it is a service directory and a public one: Let’s Encrypt uses it to confirm that the domain is yours when it issues and renews a free TLS certificate. Close hidden files “across the board” and the certificate will stop renewing — and about three months later the site will greet visitors with a browser warning about an insecure connection. The (?!well-known) construct in the rule means exactly one thing: “everything starting with a dot, except well-known”.

A couple of practical notes:

Backups in the root do not start with a dot, so they are closed by a separate rule:

location ~* \.(sql|zip|tar|gz|bak|old)$ {
    return 404;
}

Before adding it, make sure the site does not serve such files legitimately: if you have a section that offers a price list as a .zip, it will stop working. Better still, do not keep dumps and archives inside the public folder at all — move them one level up.

Apache

For Apache 2.4, a single line in the .htaccess file in the site root is enough (create the file if it does not exist):

RedirectMatch 404 /\.(?!well-known)

The logic is the same: any address containing a hidden file or directory gets a 404, while .well-known keeps working. Backups are closed separately:

<FilesMatch "\.(sql|zip|tar|gz|bak|old)$">
    Require all denied
</FilesMatch>

If your hosting forbids .htaccess or ignores the directives in it, the rule has to go into the virtual host configuration — that is a job for the hosting provider or a system administrator.

The right answer: do not publish service directories at all

A rule in the config is insurance, not a solution. The solution is for .git and .env not to physically sit in the public part of the site:

If the directory was exposed: closing access is only half the job

The most common and most expensive mistake at this step is to stop here. “We added the rule, it is a 404 now, case closed.” It is not closed.

While the directory was available, anyone could download its contents, and that leaves almost no trace: an ordinary request for an ordinary file, nothing suspicious in the logs. Automated crawlers go over other people's sites constantly and stockpile what they find — they may use the harvested keys weeks after you closed everything up. You have no way to prove that nobody took a copy. So you have to assume somebody did.

Every secret that was in the code and in the history counts as compromised and has to be reissued. The practical list:

One thing apart: if a database dump with customers' personal data was lying in the root, the question goes beyond technology. Russia's personal data law requires that you notify Roskomnadzor — the federal regulator that supervises the processing of personal data — about the incident, and the deadlines there are measured in days, not weeks. This is the case where you should bring in a lawyer straight away rather than work it out on your own.

Reissuing keys is several hours of work and almost always comes with downtime for integrations. The temptation to decide that “probably nobody got there in time” is strong. But the cost of being wrong is asymmetric: reissuing keys for nothing costs you an evening, while failing to reissue them in time costs you charges on your payment key and a mass mailing under your name a month later, when nobody will connect it to an exposed .git any more.

How to keep it from happening again

  1. Add .env and files like it to .gitignore — then they never physically reach the repository. Keep an .env.example next to it with the same setting names but empty values: a new developer sees what has to be filled in, and no secret appears in the history.
  2. Secrets belong in environment variables or in your hosting provider's vault, not in code files. The rule is simple: if a value cannot be shown to an outsider, it must not reach the repository even for a minute.
  3. Different keys for staging and production. The test environment is almost always protected worse, and that is where keys leak from most often. If the keys are the same, a leak from staging is a leak from the live site.
  4. Check by hand after every migration and every release. Changing hosting, changing contractors, moving to a new server — those are the three situations in which .git comes back most often. The check takes a minute.
  5. Set up a recurring external check. A manual check is good exactly once: it says nothing about the file that lands in the root six months from now, after a rushed Friday release. Only repeated monitoring is worth anything.
  6. If one such file turns up, look for the rest. They share a cause, so they rarely come alone.

Check your site for free

ShieldSafe looks for service files exposed to the internet — .git, .env, forgotten dumps and debug pages — as well as known vulnerabilities in the component versions you use. Every finding is explained in plain language, with wording you can hand straight to a developer. Nothing to install on your server.

Run a check