Back to more research

API keys, bank details, disciplinary files: what 28,000 exposed git repos gave up

API keys, bank details, disciplinary files: what 28,000 exposed git repos gave up
Tom Steer
Tom Steer
Security Consultant

We scanned the internet for a simple but often severe exposure: wide-open .git repositories that leave sensitive credentials for key systems exposed to anyone who finds them. It's a basic mistake, and organizations are still making it at scale.

Scanning at this scale hits limitations with traditional tools, which require reconstructing the entire repository on disk before you can search it. This works for one target, but at scale it means terabytes of storage just to look for a leaked key. So we built gitreaper, a tool that walks a repository's history in memory, checking each host for exposed credentials and saving only the secrets it uncovers.

Here's what we found when we pointed gitreaper at 3.5 million live hosts.

Scanning 3.5 million hosts

Intruder monitors and stores a copy of customers’ Certificate Transparency (CT) logs as part of our attack surface management capabilities. We've written before about how we use this data to scan the internet. Querying it for hosts on interesting subdomains such as app, admin, and dev gave us a list of domains with a reasonable probability of hosting a repository: a total of 40 million potential targets.

To find out how widespread this problem actually was, we needed to scan at that scale ourselves. Using Packer images and Celery to distribute scanning jobs across our scanner fleet let us probe tens of millions of hosts far faster than a single machine could manage. httpx filtered our 40 million hosts down to 3.5 million with an active HTTP service, then Nuclei checked each for exposed repositories.

That's where we hit a problem. Traditional tools for working with exposed git repos require downloading the full contents of the repository before processing. That's fine for a single target, but this wouldn’t work for scanning every exposure we’d found. A git repository can range from a few megabytes to tens of gigabytes, and at our scale that could mean terabytes of storage just to check for a leaked key. This is what led us to build our own tool.

How gitreaper works

gitreaper walks a target's commit history entirely in memory. Each blob is pulled from the target repository, checked against our rule set, then dropped, so nothing is written to disk unless it actually contains a secret. This saves us from writing tens of thousands of full repositories to disk purely to scan them. Importantly, walking the entire history rather than just the latest HEAD state means we still catch secrets in files that were removed in later commits. This is a common mistake, and frequently where the most impactful leaks are found.

To explain how that's possible, it helps to know a bit about how git stores things. A repository isn't really a set of files and folders, it's a database of objects, each identified by the SHA-1 hash of its contents. There are three object types we care about: commits (a snapshot pointer, with metadata and a link to the previous commit), trees (a directory listing, mapping filenames to other trees or blobs), and blobs (the actual file contents, with no filename attached, that's stored in the tree that points to it). A file's history is just a chain of commits, each pointing to a tree, each tree pointing to blobs and other trees, all the way down.

The first challenge is knowing where to start, since directory listing on web servers is almost always disabled, so we can't just browse the folder to see what's there. Instead, gitreaper tries a sequence of known file paths that git itself creates and relies on:

  • HEAD, a small text file that points to whichever branch is currently checked out.
  • packed-refs, a file git uses to store many branch and tag pointers compactly, once a repository accumulates enough of them.
  • Common branch names such as main, master, and develop, guessed directly, in case the above files aren't exposed or don't resolve.
  • Reflogs - git's local log of where branches have pointed to over time. These matter because they can retain commit hashes for commits that no branch currently points to - for example, a commit that was on a branch that's since been deleted or rewritten. If that commit is still sitting in the reflog, we can find our way to it even though nothing in the current repository state references it anymore.

Any of these give gitreaper a starting hash. From there it walks the commit graph: fetches a commit, reads its parent hash out of it, queues that one, and so on, building out the full commit history. 

Each commit points to a full snapshot of the repository, but most commits only change a handful of files, so fetching every tree and blob referenced by every commit would mean re-downloading the same unchanged files repeatedly. Instead, gitreaper tracks every tree and blob hash it's already fetched and skips anything it's seen before. Since git objects are addressed by content hash, a file that hasn't changed between commits has the same hash both times, so it only gets fetched once, no matter how many commits or branches reference it.

Fetching the objects themselves is another problem. Git sometimes compresses a repository's objects into pack files. A list of these is usually reachable through objects/info/packs, a manifest file that git maintains listing each pack's name, so gitreaper can fetch the pack and its index directly. Where objects haven't been packed at all, they exist as individual loose files on disk, one per object, named after its hash, and gitreaper fetches those directly.

Once a blob is in memory, it's scanned against our secrets detection rules and then discarded if it doesn't match. Only files that trigger a detection are ever written to disk. Some rules carry their own filtering for false positives: entropy checks, placeholder and template detection, and runtime-value detection. That lets us filter aggressively on generic-looking patterns without losing genuine secrets.

What we found

In short, here’s what we found:

  • 3.5 million active hosts 
  • 28,000 exposed repositories
  • 400 AWS access keys
  • 107 Stripe API keys
  • 123 OpenAI API keys
  • 80 Telegram tokens
  • 17 GitHub personal access tokens
  • And a lot more

Many of the credentials we extracted were still active at the time of testing, which could in some cases allow an attacker to gain full access to exposed cloud environments. 

Once scans were done, we sifted through as many results as possible and identified several very sensitive exposures. We also found numerous indicators of prior compromise, especially within S3 buckets, where we observed a significant volume of unauthorized files and buckets.

AWS access keys

The most impactful exposures were the AWS access keys we recovered, of which we found over 400.

For example, AWS keys were found hardcoded in a Python settings file, with access to a bucket of internal employment documents, including attendance records and disciplinary files. Exposure of such highly confidential records provides a prime opportunity for adversaries to orchestrate targeted social engineering campaigns.

An AWS key was also found in a previously committed .env file. These credentials had access to a music hosting collection - while not as sensitive as some of our other findings, this is a clear reminder that hardcoded credentials in a repository can grant far more access than a developer intended, whether that's a personal side project or a company's most sensitive data.

Stripe API keys

107 live Stripe secret keys were recovered, a handful of which were still valid. Depending on the permissions of the keys, a live secret key can allow an attacker to refund purchases, extract customer personally identifiable information (PII), or redirect payouts to an attacker-controlled account.

A secret key was found in a committed config file that allowed us to view a company’s payout and transaction history, exposing revenue figures, payout schedules, and partial bank account details.

GitHub personal access tokens

17 GitHub personal access tokens (PATs) were recovered, several of which were still valid. Depending on their scope, a live PAT can grant access to an organization's private repositories, letting an attacker pull proprietary source code or push malicious commits of their own.

What this teaches us

With this small sample of the internet, it's plain to see that exposed git repositories remain a widespread, unsolved problem. These are easy misconfigurations with a single well-known path sitting open on production infrastructure, often for years, leaking credentials that grant real access to real systems. And the risk is growing. Last month, OpenAI's AI models broke out of a sandbox and used publicly exposed credentials to help compromise Hugging Face's infrastructure. When autonomous agents can find and exploit these mistakes without human involvement, the window between exposure and compromise gets a lot shorter.

Try gitreaper

gitreaper is open source and available now on GitHub. It was built for this research rather than polished for general use, so expect some rough edges. It's built to run at scale, and works just as well for checking a single target as it does for scanning thousands of hosts.

Responsible disclosure

The scope of the problem was huge and far worse than we’d anticipated, but we responsibly disclosed exposures where we were able to identify the owner and find a contact. Many of the affected repositories have since been taken down, and several organizations rotated the credentials we flagged.

How can Intruder help?

Intruder uses a wide range of scanners to detect attack surface exposures and report them as vulnerabilities, including .git repositories. The example in this post is just one of many thousands of different issues that could be sitting there on the internet, waiting for an attacker to find them. Intruder develops custom checks using Nuclei to detect them, so all you need to do is run a scan and follow the remediation advice in your results. Get started for free