Truffle Security pulled 1,103,438 credential exposures out of public GitHub repositories and tested each one against the service that issued it. 543,699 still authenticated. The median one had been sitting in a public default branch for 784 days.
The report went up on October 1. The corpus is The Stack v3, an AI training dataset whose crawl closed on August 7, 2025: 224,553,295 repositories and 58,467,468,698 files. Truffle ran its verification on July 27 and 28, 2026. Every key in that count had been public for a year before anyone checked whether it still worked, and most for far longer. The 90th percentile is 6.3 years. The oldest live credential was last modified on June 13, 2009, which made it 16.1 years old on the day it let someone in.
Half a million working keys is the headline. The breakdown is the part worth your time.
Revocation decides whether a leak matters
Truffle counted survivors by credential type. The spread is not close.
- npm tokens: 1 live out of 101,886 exposed
- Hugging Face tokens: 15 out of 30,437
- GitHub tokens: 260 out of 73,048
- AWS access keys: 6,819 live, an 8% survival rate
- Google Cloud service accounts: 69,041 out of 126,963
- Postgres connection URIs: 11,465 out of 12,985
- MongoDB connection strings: 51,067, every one of them still working
npm killed 99.999% of its leaked tokens. MongoDB strings survived at 100%.
Detection is identical across those rows. A scanner finds an npm token and a Postgres URI with the same confidence. What differs is what happens after the finding. npm, GitHub and Hugging Face take alerts from GitHub's partner program and kill the token on their own side without waiting for the developer to wake up. There is nobody on the other end of a MongoDB connection string. The database is yours, the credential is yours, and the only revocation path is a human noticing and rotating it.
I would put that in front of anyone who thinks a secret scanner is secret management. A scanner produces an alert, and an alert is a request that a person do something. For 11,465 Postgres databases, no person did it, for a median of two years.
Push protection halves one half of the problem
GitHub turned push protection on by default for public repositories in 2024. It blocks a commit when it recognizes a credential in the diff. Truffle grouped the live credentials by when they landed.
- Before free alerts, meaning before February 28, 2023: 245,959 credentials, 45.2% of the total
- Free alerts with push protection optional, February 2023 to February 2024: 97,897, or 18.0%
- Push protection on by default, after February 29, 2024: 199,843, or 36.8%
Just under 200,000 live credentials reached public GitHub after the block was on by default. The block works on what it knows. Comparing the twelve months before rollout against the twelve after, the credential families push protection recognizes fell 53% to 57%. The shapes it does not recognize fell 7% to 15%. And 51.8% of everything still live is one of those unrecognized shapes.
Database connection strings and Google API keys are the two big unrecognized ones. A Postgres URI is a scheme, a user, a password and a host. It looks like a URL, because it is one. You cannot write a detector for that pattern you would trust to block a push without also blocking every tutorial, every test fixture and every docker-compose example in every repo on the service. So GitHub does not block it. And 88% of the ones in public code are live databases.
I maintain an open source backup server, so this problem arrives in bug reports. A config file holding a database URI and an object storage key is the normal case for self hosted software, not the exception. Those files get copied into repos to share a reproduction. The credential inside has no issuer watching for it.
784 days against a four day exploit window
Google's Threat Intelligence Group published its own numbers on September 30. CVE disclosures ran 5,045 in January 2026 and 10,740 in August. Vulnerabilities exploited in the wild averaged 10.5 per month across 2025 and 18 per month from January through August 2026. CVE-2026-1731 in BeyondTrust Privileged Remote Access, a bug Hacktron AI found, was exploited four days after public disclosure, with five more threat clusters arriving inside a week.
Set the two reports side by side. Attackers close on a fresh CVE in four days. A leaked database URI sits in public for 784. That asymmetry has nothing to do with how clever either side is. Scanning public GitHub for credentials has been cheap for a decade and anyone can do it. The 543,699 number exists because nobody with the authority to turn those keys off turned them off.
Which also means the usual framing is backwards. Teams buy detection and measure themselves on how fast an alert fires. The number that predicts your exposure is what fraction of your credential types die on their own.
Rules I would hold a team to
Scan history, not pushes. Push protection never looked at anything committed before February 2024, and 45.2% of the live credentials predate even the free alerts. A repository nobody has scanned end to end is unscanned, whatever its current settings say. Truffle makes this point and it is the one teams skip, because scanning history means finding things and finding things means work.
Treat a committed credential as burned. Not rotate if the alert fires. Rotate. The gap between a push to a public repo and the first automated clone is short enough that no threshold is useful.
Prefer credentials that expire without anyone's help. AWS quarantines access keys it finds exposed in public repositories, and 8% of the ones Truffle tested still worked. Google Cloud service account JSON has no equivalent, and 54% still worked. A token with a one day life that leaked in 2009 does not show up in a 2026 scan. A JSON key file is permanent by design, which is the design problem.
Check whether your vendors revoke. GitHub publishes the partner list. If something you depend on is absent from it, a leak of its credential is yours to catch and yours to fix, and the 88% figure tells you how that usually goes.
One prediction you can hold me to. The next run of this study will show the same split: recognized shapes down again, connection strings flat. The gap is not a detection problem waiting on better pattern matching. It closes when Postgres, MongoDB and the rest ship short lived credentials as the default path, so that a string pasted into a public repo expires on a schedule instead of waiting 784 days for someone to remember it.