← All posts

4,500 websites leaked their source code in 2024. Here's how many still do in 2026.

In 2024, Truffle Security counted 4,500 top-1M websites exposing their .git directory. We re-ran the measurement in 2026: 3,618 sites still serve theirs. Same bug, same one-line fix, fifteen years on.

Two robots on a pedestal, one older and one newer, each with a small imp grinning from an open drawer in its side, while a developer inspects them with a magnifying glass

#Preamble

In August 2024, Truffle Security published a study that put a number on a class of security misconfiguration everyone in AppSec already knew about but nobody had recently measured. They scanned the top 1M websites and found 4,500 of them exposing their .git/ directory to the public internet. Not just directory metadata: actual .git/config files, sometimes with live credentials.

The class of bug is not new. Earlier scans in 2018 found similar numbers. Every bug bounty write-up on exposed .git/ has been published somewhere. The fix has been the same one-line web-server rule for a decade.

Two years after Truffle's paper, the question I wanted to answer is simple: did we clean up?

Below is the 2026 number. The same basic check, using a current top-sites corpus, same public exploit tools (git-dumper, GitTools) that turn an exposed .git/ into a full source tree.

#Method

  • Corpus: CRUX Top 1M, the Chrome pageview-volume list that replaced the deprecated Alexa Top 1M. Snapshot from 2026-07-23.
  • Upstream filtering. After upstream filtering (non-resolving hosts, no HTTPS) and the probes themselves, 61,463 hosts returned HTTP 200 with a downloadable body — our effective validation set. Truffle Security's 2024 study almost certainly did the same class of filtering — the exact numbers weren't published, but for any comparable scan the "top 1M" headline never means all one million hosts get probed.
  • Probe: For each remaining host, a GET request to https://<host>/.git/config and https://<host>/.git/HEAD. Timeout 5s. No redirects followed cross-origin.
  • Detection signal: Not just HTTP 200. The response body has to look like a real git file — [core] for .git/config, symbolic-ref or hash for .git/HEAD. Exact matchers are in the scanner source. In practice, only ~6% of /.git/config responses that returned 200 turned out to be real — the rest were SPA index pages, WAF challenges, or soft-404s.
  • Scope: presence + light structural parsing. We count files that exist and pass the content-signature check, and we parse the [remote "origin"] URL host to categorise where the code lives (GitHub, Bitbucket, self-hosted, etc.). We do not extract credentials, do not test any credentials, do not clone git trees, and do not fetch additional files beyond the two probed paths.

Full scanner is published as part of webcensus for anyone who wants to reproduce or extend.

#Numbers

#Top-line comparison

Truffle 2024:

  • Corpus: Alexa Top 1M
  • Hosts with a downloadable HTTP 200: not published
  • .git/config confirmed exposed: ~4,500

This scan, 2026:

  • Corpus: CRUX Top 1M
  • Hosts with a downloadable HTTP 200: 61,463
  • .git/config confirmed exposed: 3,618 hosts (3,099 unique root domains)
  • .git/HEAD confirmed exposed: 3,675 hosts

Two ways to read the number:

  • 3,618 hosts if we count anything with its own subdomain. This is the closest apples-to-apples with Truffle's 2024 headline.
  • 3,099 unique root domains after grouping subdomains under their parent (e.g. store.example.com and blog.example.com count as one — example.com). Very close to the hostname count — subdomain clustering is minimal in this data.

Both numbers are down from Truffle's 4,500, but not by much. Two years later, this class of bug is still very much alive. Let's look at the shape of the leaks.

#Where the exposed code lives

Of the 3,618 confirmed exposures, here is what they point to as their [remote]:

  • GitHub — 1,608 (44.4%)
  • Self-hosted or other named git hosts (git.*, code.*, scm.*, plus regional / corporate / niche hosts that aren't the big three) — 564 (15.6%)
  • Bitbucket — 429 (11.9%)
  • GitLab.com — 372 (10.3%)
  • No [remote] block configured — 250 (6.9%)
  • Self-hosted GitLab — 155 (4.3%)
  • Multiple remotes — 87 (2.4%)
  • Bare IP address as remote — 70 (1.9%)
  • Local filesystem path (file:///, /home/…) — 44 (1.2%)
  • AWS CodeCommit / Azure DevOps / Gitea / Gitee (combined) — 39 (1.1%)

Two takeaways.

Major SaaS git hosts account for ~68% of the total (2,448 out of 3,618). Inside that group:

  • GitHub — 65.7%
  • Bitbucket — 17.5%
  • GitLab.com — 15.2%
  • AWS CodeCommit / Azure DevOps / Gitea / Gitee (combined) — 1.6%

GitHub's dominance is expected — that is where most public code lives. Bitbucket outweighing GitLab is a mild surprise. AWS CodeCommit and Azure DevOps show up in the tail — small, but not zero.

The remaining ~32% is where leaks actually hurt. Self-hosted GitLab instances, generic git.* / code.* remotes, corporate and niche git hosts, bare-IP-address remotes on legacy servers, file:///… local paths on hand-rolled deployment machines, and configs with no [remote] block at all. When those leak, the code behind them is more likely to be genuinely private. The 44 configs pointing at a local filesystem path are a particularly clear signature: someone deployed a directory whose upstream was a local repo on some server. That is not a "we forgot to .gitignore the deployment" story. It is a "we push to prod from a shell" story.

#No single villain

Each of the 3,618 leaking hostnames sits under a parent domain. Grouping by parent instead of by hostname, we get 3,099 distinct ones. Then we can ask: is the damage concentrated in a handful of offenders, or spread thinly across many?

  • 93% of affected parent domains leak exactly one hostname. One team, one mistake, one exposed .git/config.
  • The single worst offender contributes 42 leaks — subdomains all under the same parent. That is only 1.16% of the total.
  • The 10 worst offenders combined: 4.83% of the total.
  • The 100 worst combined: 14.3%.

A very flat distribution. This isn't the story of one careless hosting platform spraying .git/ everywhere. It's the story of thousands of independent teams, on independent stacks, making the same mistake.

Which means the fix belongs at the origin — one line of nginx / Apache / Caddy config that denies /.git/, or a deployment pipeline that doesn't ship .git/ to prod in the first place.

#.git/HEAD: a second probe, nearly identical results

To stay close to the work done by the Truffle Security team, we also probed /.git/HEAD. We initially thought the result might differ if defensive rules targeted .git/config specifically.

It did not. We confirmed 3,675 hosts exposing .git/HEAD, compared with 3,618 hosts exposing .git/config, and roughly 98% of .git/config-exposed hosts also exposed .git/HEAD. The small difference did not materially change the finding. In this dataset, probing HEAD mostly validated the config result rather than revealing a separate pattern of protection.

Practical read: the near-identical results suggest that defensive rules commonly treat /.git/ as a directory rather than targeting .git/config alone. However, this scan cannot determine how any specific WAF, CDN, or origin server was configured.

#The 32% that returned 403

32% of our requests to /.git/config returned HTTP 403, meaning the request was denied somewhere along the delivery path.

We cannot tell from this scan whether the block came from a CDN, a WAF, the origin server, or another access-control layer. We also cannot tell whether a real .git/config file exists behind the block. Our probe log stores response codes, not the response headers needed for reliable attribution.

The finding is still meaningful: a substantial share of .git/config probes are denied before they produce any content. Whether that denial is a real defensive layer (a CDN blocking .git/ before it hits the origin) or a coincidental non-answer (the origin returning 403 because the file was never there) is not something this scan can distinguish. The 403 pool remains an unknown category — worth reporting, not worth over-interpreting.

#The false-positive story is also part of the finding

Of the 61,463 hosts that returned HTTP 200 with a downloadable body, only 3,618 were real .git/config files. That is ~6% real, ~94% noise. Every previous "we scanned N sites and found M exposed" number in this space is worth re-checking against a content signature. Naive 200-only scans overstate by ~17×.

If you're a defender running your own check, don't trust HTTP 200 on /.git/config. Grep for [core] in the body. Anything else is an SPA fallback, a WAF challenge page, or a soft-404.

#What we didn't do

Scope and ethics, briefly:

  • No git-tree cloning. Only two paths (/.git/config and /.git/HEAD) were requested per domain. No source code, no commits, no blobs were retrieved.
  • No credential extraction. We checked the content signature to confirm each file was a real git config, and we parsed the [remote "origin"] URL host — nothing more — to categorise where the code lives. No other parts of the file were parsed, stored, or acted on. No credential values were extracted, tested, or logged.
  • No individual victim identification. All aggregate. This is a presence measurement, not a credential audit.
  • Scanner is limited to two requests per domain, one for each specified path (/.git/config and /.git/HEAD), with a 5-second timeout and no retries. Not a DoS shape.

This is the same methodology and ethical envelope as Rapid7 Project Sonar, Censys, and Shodan — passive presence measurement of publicly-served URLs. No further exploration beyond confirming the file exists and is what it looks like.

A maintenance robot on a stepladder sealing another robot's side drawer shut with a screwdriver, while a defeated imp stands nearby with an empty suitcase

#What defenders should do

The fix has been the same for a decade. Nothing new. Publishing this list again because clearly not enough teams have run it:

  1. Web server: deny /.git/ at the reverse proxy / web server layer. Nginx:

    location ~ /\.git { deny all; return 404; }

    Apache: <DirectoryMatch "\.git">Require all denied</DirectoryMatch>. Same rule for .env, .svn, .hg, and the AI-assistant config files that developers now leave lying around (.claude/, .cursor/) — I wrote about that class in Your AI coding assistant is quietly shipping your secrets.

  2. Deployment pipeline: do not copy .git/ into the production artefact. Every modern build system supports .dockerignore / .gcloudignore / .gitignore on the deployment side. Verify it.

  3. CI check: add a post-deployment test that hits /.git/config on your prod URL and fails the deployment if the response isn't a 4xx. One line of curl.

  4. If you find .git/ was ever exposed, assume your source code is already leaked. Not just the config file. The whole tree — commits, history, every branch. Prioritise your SAST and SCA results, because attackers can now read the code for themselves and find issues faster than you can. Rotate every credential that has ever been in the repo, including ones in old commits.

  5. Check your CDN / reverse proxy configs. In many cases, the origin server blocks /.git/ but a CDN with a stale cache still serves it. Test through the CDN, not just origin.

None of this is new advice. Every web-security scanner (Nikto, Nuclei, Burp, ZAP) has this check built in — turn one on and you'll spot it in the first pass.

#Closing

The class is 15+ years old. The fix is one line of web-server config. The tooling to exploit it (git-dumper, GitTools) has been public and mature since 2016. And in 2026, the count is still ~3,600 on the top web-traffic list.

The confirmed count came down a little. That does not necessarily mean developers learned. The difference could reflect actual remediation, increased blocking by CDNs and WAFs, changes in the underlying corpus, or some combination of the three. This scan cannot tell us which.

Regardless of which story it is, the 3,618 confirmed exposures are the sites where no effective control stopped the file from being served. Wherever any blocking layer does exist, it only helps as long as it's actually in the request path and correctly configured — that's what "not fixed" looks like from the attacker's side.

Ask again in 2028.


#References

Share