Database audit fix

How to Block Public Access to .git in Web Root

A .git directory left inside the web root lets anyone fetch /.git/HEAD and rebuild the whole repository, including every credential ever committed. Deploy built artifacts instead of the checkout, and deny dotfiles at the server.

Hiren KalariyaLast reviewed: Aug 30, 2026Check git-in-web-root
High
severity
No
needs root or sudo
2
results it can return
3 of 4
checks in this audit

Every threshold on this page is transcribed from the database-security-credential-storage audit script that CtrlOps runs, and a build check fails if the two ever disagree. See all 4 Credential Storage checks, or how the audit runs.

High severityNo sudo needed

What this check reads

The check runs a single ls -d against five fixed globs: /var/www/.git, /var/www/*/.git, /var/www/*/*/.git, /srv/www/*/.git and /usr/share/nginx/html/.git. Nothing inside a repository is opened, and no HTTP request is made; the presence of the directory on disk is the whole test. Whatever the globs expand to is truncated to the first three entries (head -3) and joined onto one line, so an empty result is the PASS and any entry at all is a WARN that names the paths it kept.

When it applies

Always runs - a plain ls -d on the disk, no root and no database needed. It looks only at these fixed globs: /var/www/.git, /var/www/*/.git, /var/www/*/*/.git, /srv/www/*/.git, /usr/share/nginx/html/.git, and reports at most three. Deeper checkouts and other roots are not matched. A .git file (the gitdir pointer left by worktrees and submodules) is matched, because ls -d applies no -type d filter, so one sitting at any of those five positions is reported exactly like a directory. This check looks at the disk only; whether the server actually serves /.git/ is the separate Hidden File Exposure web check - which is why the status here is WARN rather than FAIL.

What each result means

Result thresholds this check applies
ResultWhenWhat it means
WARNAny `.git` directory found under the common rootsA repository checkout is sitting in a served tree, and the message lists up to three of the paths found.
PASSNoneNone of the five globs matched, so a checkout deeper in the tree or under a different web root would not have been seen.

Why it matters

A .git directory inside a served tree lets anyone download /.git/HEAD, /.git/config and, with tools like git-dumper, reconstruct the complete repository including history and any credentials ever committed. Even with directory listing off, the object layout is predictable. Research on the Alexa top 1M found thousands of exposed repositories. OWASP WSTG-INFO-03 "Review Webserver Metafiles"; OWASP Top 10 A05:2021.

Why it fails, and when it is wrong

  • "Deploy by git pull in the document root" is the common cause. The web-server check Hidden File Exposure tests whether the server actually serves /.git/HEAD; this check only looks at the disk, which is why it is WARN.
  • Depth is limited to three levels under /var/www and one under /srv/www. Deeper or non-standard roots are not checked.
  • A .git file (gitdir pointer used by worktrees/submodules) is still listed: ls -d reports whatever the globs expand to, with no -type d filter. Only one sitting outside the five globs is missed, and it is just as dangerous if served.

How to fix it

Find the directories

Reproduce the check to get the paths it kept, and widen the search if your document root is not one of the five globs it knows:

ls -d /var/www/.git /var/www/*/.git /var/www/*/*/.git /srv/www/*/.git /usr/share/nginx/html/.git 2>/dev/null
find / -xdev -maxdepth 6 -name .git -print 2>/dev/null

Test whether it is actually reachable

The check looks at the disk only, so ask the web server the question it cannot:

curl -sS -o /dev/null -w '%{http_code}\n' https://your-host/.git/HEAD

A 200 means the repository is downloadable with a tool like git-dumper, history included. A 403 or 404 means only the server config stands between you and that. If credentials were ever committed, rotate them either way: removing the file does not remove it from history, and anything exposed was fetchable for as long as it was served.

Block it at the web server

Block dotfiles as defence in depth, then reload the server:

location ~ /\.(?!well-known) { deny all; return 404; }
RedirectMatch 404 /\.(?!well-known)
# or
<DirectoryMatch "^\.|\/\.">
  Require all denied
</DirectoryMatch>

Deploy so it cannot happen again

A deny rule is one config change away from being gone, so stop the checkout reaching the served tree at all. Deploy with a build step that copies only the artifacts (git archive, rsync --exclude .git, a CI deploy), or keep the checkout outside the root and symlink or point root at public/.

Verify the fix

# On disk - reproduce the check:
ls -d /var/www/.git /var/www/*/.git /var/www/*/*/.git /srv/www/*/.git /usr/share/nginx/html/.git 2>/dev/null
# expected: no output

# Over HTTP - the exposure that actually matters:
curl -sS -o /dev/null -w '%{http_code}\n' https://your-host/.git/HEAD
curl -sS -o /dev/null -w '%{http_code}\n' https://your-host/.git/config
# expected: 403 or 404 for both (200 means the repository is downloadable)

Debugging

The repository is on disk but not reachable over HTTP, so the immediate risk is low. Keep the deny rule and move the checkout out of the served tree when convenient; a future handler or vhost change can expose it.

The checkout is outside the globs the check knows (deeper than two levels under /var/www, or a different root entirely). Find it with find / -xdev -maxdepth 6 -name .git -print 2>/dev/null and compare against your root/DocumentRoot values.

Worktrees and submodules leave a .git file containing gitdir: …. ls -d on these globs still lists it, but a deeper or differently-placed one is missed; it is just as dangerous if served. Search with find <webroot> -name .git (no -type d).

No. Anything ever committed (passwords, keys) was downloadable for as long as it was exposed. Rotate those credentials; deleting the directory does not undo the disclosure.

Sources

How the script reads this

Next

Re-run the Credential Storage audit after applying the fix and confirm this check moves to PASS. CtrlOps runs all 4 checks over your existing SSH connection and scores the result, so the change is visible without reading another config file.

All 4 Credential Storage fixes
Audit your fleet

Find every one of these on every server, in one click

CtrlOps runs this audit over your existing SSH connection - no agents, no scripts to manage. $7/user/month after a 1 month free trial - no credit card required.

Start instantly· No credit card· No sneaky autorenewals