Exposed .git directories: how source code leaks from production
A deploy that copies your repository into the web root can expose its entire history — source, config, credentials. How attackers find and download it.
Version control lives on developer laptops and build servers, not on the public internet — but every so
often a whole .git directory ends up sitting in a site's web root, one HTTP request away from
anyone. When that happens, the exposure is rarely just the current code. Git keeps the full history, so an
attacker can often recover every version of every file the repository has ever held, including secrets a
developer thought they had deleted.
How a .git directory ends up web-accessible
A Git repository stores everything it needs in a single hidden folder at the project root: .git.
That folder is not meant to be served to browsers, but two common deployment habits put it there anyway.
-
Deploying with
git cloneorgit pull. Treating the live server as a checkout is convenient — you pull to deploy — but it leaves a live.gitdirectory next to your application files. If the document root points at the repository root, that folder is now web-reachable. -
Copying the whole tree. An
rsync,scp, FTP upload, or archive extraction that includes hidden files carries.gitalong with the code. Nothing warns you; the deploy simply succeeds with an extra folder attached.
The reason it becomes fetchable is that most web servers happily serve any file under the document root
unless told otherwise, and the contents of .git are ordinary static files. No directory listing
is required — an attacker who knows Git's layout can request individual paths by name.
How the exposure is detected
Git's internal layout is completely deterministic, which makes the exposure trivial to confirm without any
guesswork. The usual tell is a request for /.git/HEAD: on a normal site that returns a 404, but
a leaked repository returns a small text file whose body looks like ref: refs/heads/main. A
request for /.git/config is a second confirmation — it returns the repository's configuration,
often including the remote URL the code was cloned from.
A 200 response carrying that recognisable Git content is unambiguous. From there the important point for defenders is what it enables, not the precise sequence of requests.
Reconstructing the source without a directory listing
Many operators assume that turning off automatic directory listings (autoindex) protects them. It does not. Because Git's structure is predictable, the object graph can be rebuilt by following references rather than browsing folders:
/.git/HEADnames the current branch./.git/refs/and/.git/packed-refsmap branch and tag names to commit hashes.- Each commit hash points at a loose object at a path derived from the hash (for example
/.git/objects/ab/cdef...), or is bundled into a packfile under/.git/objects/pack/.
Every object references the ones it depends on, so a fetcher that starts at HEAD can walk the
entire chain — commits, trees, and file blobs — pulling each one as a plain static download. This is the
"dumb HTTP" transport: it needs no cooperation from a Git server, just an ordinary web server returning
files. Off-the-shelf tools exist that automate this walk over refs and pack files and rebuild a working
copy of the repository. The result is not a snapshot of the current site — it is the project's full,
browsable history.
Why this is a critical exposure
Source-code disclosure is severe on its own. With the code in hand, an attacker no longer has to guess: internal API endpoints, authorisation logic, input-validation rules, hidden admin paths, and third-party integrations are all laid out plainly. Any vulnerability in that code becomes far easier to find and exploit with the source than by black-box probing.
The sharper danger is the history. Repositories routinely accumulate secrets that were committed and later
"removed" — an API key hardcoded during debugging, a database password in an old config, a checked-in
.env file, a private key. This is where a subtle but critical fact matters:
Deleting a secret from the latest commit does not remove it from the repository. Git is designed to preserve history. When you edit a file and commit, the old version is retained as a prior object, reachable from earlier commits. So a credential that was committed in March and "deleted" in April is still sitting in the March commit — and anyone who reconstructs the repository can read it. Scrubbing a secret from the current tree is not remediation; it only hides the value from someone reading the newest files.
How to fix it
There are three independent things to do, and all three matter.
-
Never deploy the
.gitdirectory. Ship build artifacts, not a checkout. Use a CI pipeline that copies only what the site needs, or export a clean tree withgit archiverather than cloning onto the server. Where the framework supports it, point the web server's document root at a public subdirectory (for example apublic/folder) so the repository root — and.gitwith it — sits above the served path and can never be requested. -
Block dotfiles at the web server as a belt-and-braces backstop. Deny any request for a
path segment beginning with a dot, so a stray
.git,.env, or.svncan't be served even if it is deployed by mistake:# nginx location ~ /\.(?!well-known) { deny all; return 404; }# Apache <DirectoryMatch "/\.git/"> Require all denied </DirectoryMatch> -
Rotate every secret that was ever committed. If a credential lived in the history, treat
it as compromised and change it at the source — issue new keys, reset passwords, revoke tokens. Rewriting
history to purge the value (with tools such as
git filter-repoor BFG, followed by a force push) is worth doing for hygiene, but rotation is the part that actually protects you, because you can never be sure who already pulled a copy.
The wider point: secrets don't belong in code
An exposed .git directory is one way credentials leak, but the underlying lesson generalises.
Secrets embedded in a repository — or in shipped HTML and JavaScript — outlive the commit that removed them
and travel wherever the code travels. The durable fix is to keep them out of version control entirely,
supplying them through environment variables or a secrets manager, and to scan for accidental commits before
they reach production. This is closely related to other information-disclosure and configuration issues, from
missing HTTP security headers to the
outdated plugins and themes that so
often ship a wider attack surface than intended.
How Cyber Report checks this
An external Cyber Report scan probes for the tell-tale Git paths — /.git/HEAD and
/.git/config — as part of its sensitive-files audit, and flags an exposed .git
directory as a critical finding. When exposure is confirmed, the scan attempts to reconstruct the history
over dumb HTTP and runs a git-history secret scanner across it to surface any committed API keys or
credentials. To avoid ever exfiltrating your secrets, we deliberately do not validate candidate credentials
against their issuers — findings are reported so you can rotate them, never tested against a live provider.
See what we scan, or run a scan on your own domain to see where you stand.