dissecting a git-exposure finding: anatomy of an information leak

target: self-hosted demo · medium

Site fixture. This post exists to pin the research template to something concrete. The target is a demo app I stood up on my own box; no third party was tested, and the leaked credential below is a dead fixture token. The bug class, the requests, and the reconstruction steps are real and reproducible.

TL;DR

  • What: the demo app serves its .git directory over HTTP. GET /.git/config returns 200 with the repository config.
  • Impact: full source disclosure (working tree + history), plus any secret committed or embedded in a remote URL. Here: a deploy token in the origin remote.
  • CVE status: none — self-hosted fixture. On a real target this class is CWE-527 (Exposure of Version-Control Repository to an Unauthorized Control Sphere); severity medium here, scaling with what the repo contains.

Target & scope

Self-hosted demo: nginx 1.25 serving a static export of a small app at http://127.0.0.1:8080/, deployed by a script that rsyncs the working tree into the webroot. Scope was my own machine only.

Root cause

Two failures compose:

  1. The deploy step copies the working tree.git included — instead of a clean export:

    # deploy.sh (fixture)
    rsync -a --delete ./app/ /var/www/demo/   # .git rides along
    
  2. nginx has no dotfile deny rule, so anything under the webroot is servable:

    server {
        listen 8080;
        root /var/www/demo;
        # nothing blocks /.git, /.env, /.svn
    }
    

Neither is exotic. That is the point: this bug is a default, not a mistake someone has to work at.

Exploitation

Step one is a single request. No tooling beyond curl:

From there, reconstruction is mechanical — mirror the directory, check out the tree, read the history:

Two things fall out: the full source, and — because “remove the secret” commits never remove secrets — the token survives in history even after it is scrubbed from HEAD.

Remediation

  • Deny dotfiles at the edge. For nginx: location ~ /\.(git|svn|env|hg) { return 404; }
  • Deploy a clean export (git archive, a build artifact, a CI checkout with --depth 1 into a temp dir), never the working tree.
  • Rotate anything the repo ever contained. The fixture token above is dead by construction; a real one is compromised the moment /.git is reachable, and history scrubbing does not un-leak it.
  • Detect it yourself first: curl -s -o /dev/null -w "%{http_code}\n" https://your-host/.git/config in the deploy pipeline; fail the deploy on anything but 404.

Disclosure timeline

Fixture timeline — the format, not a real vendor dance. On a third-party finding this table tracks report → acknowledgment → fix → publish.

date (UTC)event
2026-08-01demo target stood up with the misconfiguration
2026-08-01exposure found during enumeration (/.git/config → 200)
2026-08-02repository reconstructed; impact scoped (source + fixture token)
2026-08-02fix deployed (dotfile deny + clean export); re-tested → 404
2026-08-06published

Artifacts

Reproducer scripts and the demo-target definition live under github.com/skeletonsec. If you run this against anything, run it against your own deploy pipeline — that is where the class actually bites.