Home/Journal/Teardown
TeardownHighgit-exposuresecretscredentials

The .git Folder That Leaked the CRM

An exposed .git directory let the full source and its cleartext secrets be reconstructed. Here is how we found it and how to close it.

Rahul Dharan··9 min read·a real-estate CRMExposed version-control directory / secrets exposure (CWE-527/200)

The setup

The target was a multi-tenant real-estate CRM - the software a brokerage or developer uses to capture leads, track buyers, and run its sales pipeline. Its product model included per-customer microsites: each customer got their own branded lead-capture site, and those sites were deployed by cloning a git repository onto the host. One codebase, many customer-facing deployments.

That deployment shape is what drew our attention. When a site is stood up by cloning a repository, the repository can come along for the ride - including the hidden .git directory that holds the project’s entire history. Whether it does depends on how the web server is configured, and configuration is exactly the kind of detail that gets set once and forgotten across dozens of customer microsites.

Recon

Working black-box, with no account, we enumerated the customer microsites and looked at how each was served. On one of them, the web server had directory listing enabled, and the listing included a .git directory. The server was, in other words, offering the project’s version-control folder to anyone who asked for it.

An exposed .git directory is not a minor information leak. It is potentially the whole repository, sitting on the public internet in a form that can be reassembled.

The hypothesis

Git stores a project as content-addressed objects: loose objects for individual files and commits, and pack files that bundle history efficiently. If those objects are downloadable, the repository can be reconstructed offline - the current source, the history, and anything that was ever committed and not scrubbed from history.

So the hypothesis was straightforward and had a specific payoff in mind. If we could pull the objects, we would have the source. And source for a lead-capture microsite that talks to a central CRM almost always contains the secrets it needs to do that talking: a database credential, an API key, a gateway token. Deploy-by-clone tends to ship those secrets right alongside the code.

What we tried

We pulled the loose objects and pack files that the directory listing exposed and reassembled them into a working copy of the repository offline. The reconstruction succeeded: we had the full source.

The source contained secrets in cleartext. Database credentials. Per-customer production API keys - over a dozen of them across a handful of repositories. An SMS-gateway credential. These were not obfuscated or referenced from a vault; they were sitting in the committed files.

Then we did the minimum needed to answer the only question that matters about a leaked credential: is it live? We took two of the production API-key pairs and made a single read-only call each against a staff endpoint - one that rejects every caller that is not holding a valid key. Both were accepted. That confirmed the keys were real, current, and privileged, with one scoped read apiece and nothing written.

We were careful about the boundaries here. The keys were tenant-bound: each one worked only for its own customer’s data, and we confirmed there was no cross-tenant pivot from them. We exercised no writes at all. And after extracting redacted evidence, we deleted the reconstructed repositories from our side rather than keep a copy of the company’s source and secrets lying around.

One more thing belongs in this section, because leaving it out would make the work look cleaner than it was. Early in the analysis we formed a claim that a pattern in the record identifiers would let an attacker enumerate the whole database. When we tested it properly, it did not hold - the identifiers were not the clean sequence the first look suggested. We withdrew that claim. It is not in the findings, and the honest version of this teardown says why: a hypothesis that fails a real test is not a finding, and reporting it anyway would be the kind of overclaim that makes a whole report untrustworthy.

What we found

Two findings, both flowing from how the microsites were built.

The exposed repository and its secrets. Directory listing over a .git directory on a customer microsite let the loose objects and packs be downloaded and the full source reconstructed. The source held cleartext database credentials, over a dozen per-customer production API keys across a few repositories, and an SMS-gateway credential. Two of the production key pairs were verified live with a single scoped, read-only call each against a staff endpoint. The shape:

1. GET /<microsite>/.git/           -> directory listing, objects visible
2. download loose objects + packs   -> reconstruct full repository offline
3. read committed source            -> cleartext DB creds, production API
                                       keys, SMS-gateway credential
4. one read-only call per key       -> staff endpoint accepts 2 live keys

The lead-capture microsite as an unauthenticated CRM proxy. A related finding sat in the same product. The lead-capture microsites held a full CRM credential server-side, so that a lead submitted through the public form could be written into the core CRM. But holding a full-power credential behind a public, unauthenticated form turns that form into a confused deputy: the form will use its powerful credential on behalf of whoever submits, which makes it an unauthenticated read/write proxy into the central CRM rather than a one-way lead intake.

Why it happens

Deploy-by-git-clone is a genuinely convenient way to ship many similar sites. You push to the repository, you clone onto the host, and every microsite stays in sync with one source of truth. The failure is not the choice to use git. It is that cloning into a web root puts the .git directory inside the publicly served tree, and if directory listing is on, the repository is now on the internet. That is one web server setting, made once, inherited across every microsite built the same way.

The secrets in the repository are the second half of the same habit. When each microsite needs to reach the central CRM, the fast path is to commit the credential it uses. It works on the first deploy and every one after, so it never gets moved into a secrets manager. And git remembers: even a credential later deleted from the current files still lives in the history the reconstructed repository hands over.

The confused-deputy microsite is the same velocity pressure in a different spot. A lead form has to write into the CRM, so it is given a credential that can write into the CRM. Scoping that credential down to exactly “create one lead” is more work than handing it a general key, and the general key ships.

For developers

If you deploy many sites from one repository, these controls close this:

  1. Never deploy by cloning into a web root. Build artifacts and deploy those, or clone outside the served tree and serve only the built output. The .git directory should never sit inside anything the web server will hand to the public.
  2. Block the .git path at the web server. As a belt-and-suspenders control, explicitly deny all access to .git and turn directory listing off. Even if a repository ends up in the tree, the path should return nothing.
  3. Keep secrets out of the repository. Database passwords, API keys, and gateway credentials belong in a secrets manager or environment configuration, injected at deploy time - not committed. Source that leaks should leak code, not credentials.
  4. Rotate anything that was ever committed. History keeps a secret even after you delete it from the current files. If a credential was ever in the repository, treat it as compromised and rotate it.
  5. Do not give a public form a full-power credential. A lead-capture form that writes into your CRM should use a credential scoped to exactly that one action - create a lead, nothing else - so a public form cannot become a read/write proxy into your core system.

One detection step: request /.git/config and /.git/HEAD against each of your public sites. If either returns content instead of a 403 or 404, your repository is exposed.

The takeaway

An exposed .git directory does not leak a file - it leaks the whole repository, history and secrets included, in a form that reconstructs offline. Deploy-by-clone with directory listing on is the quiet way that happens, and the secrets committed for convenience are what turn a source leak into working production credentials. Keep the repository out of the web root, block the .git path anyway, keep secrets in a vault, and rotate anything that was ever committed. And when a promising claim fails a real test, withdraw it - the honesty is what makes the rest of the report worth trusting.

This teardown is one instance of a broader pattern: secrets that ride along in places attackers can read them. We wrote up the general case, and how to defend against it, in The Secret in the Page Source.

Frequently asked

What is an exposed .git directory vulnerability?
It is a web server that serves the hidden .git directory to the public, usually because a site was deployed by cloning a repository into the web root with directory listing on. The loose objects and pack files can be downloaded and reassembled into the full source code and its history, including any secrets committed to it.
Why is a leaked git repository so damaging?
The repository is not just code. It carries the project's history and often cleartext secrets: database passwords, API keys, and gateway credentials that were committed at some point. Reconstructing it hands an attacker working credentials, not just a look at the source.
How do you prevent an exposed .git directory?
Never deploy by cloning a repository into the web root, block all access to the .git path at the web server, keep secrets out of the repository and in a secrets manager, and rotate anything that was ever committed, since history keeps it even after it is deleted from the current files.

This is one finding from a harness that runs continuously. See how Greywatch finds, proves, and fixes them.

How it works →