Exposed .git Directory: Risk and Detection

An accessible .git directory allows full source code reconstruction from a web server. Learn the risk, how to detect it, and how to block it.

Exposed .git Directory: Risk & Detection

When a .git directory is accessible via HTTP, an attacker doesn't need a repository invite or a stolen SSH key. They can reconstruct your entire source code by requesting a handful of files directly from your web server. This vulnerability has appeared in breaches at major companies — in 2017, it was used to extract source code and internal credentials from multiple large e-commerce sites in what became known as the "gitscrabber" campaign.

What lives in a .git directory

Git stores the complete history of a repository in the .git directory as a series of object files and pack files. The critical files from an attacker's perspective:

  • .git/config — repository configuration including the remote URL (often contains embedded credentials: https://username:token@github.com/org/repo.git)
  • .git/HEAD — current branch reference, reveals branch naming conventions
  • .git/COMMIT_EDITMSG — last commit message, useful for reconnaissance
  • .git/logs/HEAD — commit history with author emails and timestamps
  • .git/objects/ — every file and commit ever stored, packed as compressed blobs
  • .git/refs/heads/ — branch references pointing to commit hashes
  • .git/packed-refs — all branches and tags in a single file
  • .git/index — staging area index listing all tracked filenames

From just .git/HEAD, .git/config, and the index, an attacker using a tool like GitTools or git-dumper can download every committed file.

How to reconstruct source from an exposed .git

The attack uses the git object model directly. First, read the index to get all tracked file paths and their object hashes. Then fetch each object blob from .git/objects/. Tools automate this entirely:

# git-dumper (https://github.com/arthaud/git-dumper)
pip install git-dumper
git-dumper https://target.com/.git/ /tmp/repo-dump/

# This reconstructs all committed files to /tmp/repo-dump/

The tool handles pack files, loose objects, and partial repositories. It typically recovers 90–100% of committed files even when directory listing is disabled, because it can infer object hashes from the index.

What source code exposure leads to

Once an attacker has your source code:

  • Hardcoded secrets — credentials committed directly to code (common in early commits before proper secret handling was added) remain in git history even if removed later
  • Application logic for bypasses — knowledge of how authentication, authorization, and payment flows work enables targeted exploitation
  • Internal endpoint discovery — routes, API endpoints, and internal service addresses that aren't publicly documented
  • Dependency versionspackage.json, requirements.txt, Gemfile.lock expose exact library versions, enabling targeted CVE exploitation
  • Infrastructure details — Terraform files, deployment scripts, and infrastructure-as-code committed to the repository

Detection: is your .git directory exposed?

The authoritative check is requesting the HEAD file, which exists in every git repository:

curl -s https://yourdomain.com/.git/HEAD

A valid response looks like:

ref: refs/heads/main

Any response containing ref: refs/heads/ confirms the .git directory is publicly accessible. Also check:

curl -s https://yourdomain.com/.git/config
curl -s https://yourdomain.com/.git/logs/HEAD

The config file is especially sensitive — it often contains the remote origin URL with embedded credentials. The logs file reveals commit hashes you can use to reconstruct the tree.

Checking subdirectories

Don't forget subdomains and virtual hosts. A deployment pipeline might expose .git on api.yourdomain.com or staging.yourdomain.com even if the main domain is properly configured. Test each separately.

for subdomain in www api staging dev admin; do
  result=$(curl -s -o /dev/null -w "%{http_code}" https://${subdomain}.yourdomain.com/.git/HEAD)
  echo "${subdomain}: ${result}"
done

Why this happens

The most common causes:

  • Deploying by running git pull in the document root — the entire repository, including .git/, is now in the web-served directory
  • Rsync or SCP deployments that don't exclude .git/ from the transfer: rsync -av --exclude='.git' ... is required
  • Docker images built with the full repository context and served without proper configuration
  • GitHub Pages or similar services used with Jekyll or Hugo where the build output accidentally includes the source .git
  • Misconfigured S3 static hosting where the bucket sync includes hidden directories

Remediation

Block .git at the web server

# Nginx — deny all requests to .git directories
location ~ /\.git {
    deny all;
    return 404;
}

# Apache

    Require all denied

Fix the deployment process

Blocking at the web server is a safety net. The root fix is not deploying the .git directory to web-served directories at all. Deployment options:

  • Deploy only the build artifact directory (dist/, build/, public/) rather than the full repository
  • Use CI/CD pipelines (GitHub Actions, GitLab CI) that build and deploy artifacts without copying the source repository
  • For rsync: always use --exclude='.git' and audit your scripts to confirm it's present
  • For Docker: add .git to .dockerignore

Scan git history for leaked secrets

If your .git was exposed, assume the full commit history was downloaded. Scan your history for secrets using tools like TruffleHog or Gitleaks:

trufflehog git file://. --since-commit HEAD~100 --only-verified
gitleaks detect --source . --log-opts="HEAD~100..HEAD"

Any verified secrets found should be rotated immediately, regardless of when they were committed.

Real-world incidents

The gitscrabber mass exploitation of 2017 targeted thousands of sites. Security researchers found that the attack was fully automated: bots probed for /.git/HEAD, confirmed the response, then used a reconstruction tool to download everything. Sites affected included large retail brands and SaaS companies, several of which disclosed breaches attributable to credentials found in their exposed git history.

In 2021, a penetration testing report published by a Dutch security firm documented a case where /.git/config contained a GitHub personal access token with write access to the organization's private repositories. An exposed git directory on a production subdomain gave read/write access to the entire codebase.

Common mistakes

  • Blocking /.git/ but not /.git without the trailing slash — some nginx configs miss this; test both
  • Assuming directory listing being disabled is sufficient — object files are individually addressable; directory listing is irrelevant
  • Only checking the main domain — staging and API subdomains are frequently overlooked
  • Not scanning git history after a confirmed exposure — the immediate risk is the current codebase; the longer-term risk is secrets buried in 18-month-old commits

Orb44 includes .git directory exposure in its standard outside scan, testing /.git/HEAD and /.git/config on each scanned domain and subdomain. Exposure on any subdomain generates an alert, not just the root domain.

FAQ: My .git directory has directory listing disabled — am I safe?

No. Directory listing (the Apache/Nginx feature that shows a file browser when no index.html exists) is irrelevant here. Attackers don't need to browse the directory — they request specific known paths (.git/HEAD, .git/index, .git/objects/info/packs) directly. If those paths return 200, the repository is reconstructable.

FAQ: We use a private GitHub repository. Does this still matter?

Yes. The exposure is on your web server, not GitHub. Your deployed server received a git clone or git pull and now has a copy of the repository on disk. If that copy is in a web-served directory, it's publicly accessible regardless of your GitHub repo's visibility setting.

FAQ: Can attackers get files that were deleted from git history?

If the objects are still present in .git/objects/ (not yet garbage-collected), yes. Git keeps all objects until git gc runs and prunes unreachable objects. A file removed via git rm and committed as deleted is still accessible as a blob object until the next aggressive garbage collection. BFG Repo Cleaner or git filter-branch purges specific objects, followed by git gc --prune=now --aggressive to actually remove them.

Orb44 Journal · all posts