Security Education

9 min

API key security: what actually happens when your credentials are exposed

API key security: what actually happens when your credentials are exposed

What API keys are, how they leak, what the automated attack chain looks like, and what proper credential handling actually looks like for founders building with AI tools.

What API keys are, how they leak, what the automated attack chain looks like, and what proper credential handling actually looks like for founders building with AI tools.

If you’ve been following the audit findings I share here, you’ll know that exposed credentials are one of the most consistently present findings across AI-built apps. I’ve covered how Supabase’s anon key works and what RLS misconfiguration looks like in practice, but that’s specifically a database access issue. This post is about the broader picture: API keys, what they actually are, how they leak, what happens when they do, and what proper handling actually looks like.

GitGuardian’s 2026 State of Secrets Sprawl report found that nearly 29 million new secrets were exposed on public GitHub in 2025 alone, a 34% year over year increase and the largest single year jump ever recorded. That number is worth sitting with for a moment before we get into the specifics.


What an API key actually is and why it matters

An API key is essentially a password for machine-to-machine communication. When your app talks to Stripe, OpenAI, SendGrid, or any other external service, it authenticates itself using a key. Whoever holds that key can make requests to that service with the same level of access your application has. There is no inherent way for the service to distinguish between a request coming from your app and one coming from an attacker using your key.

This is what makes them structurally different from a user password. A user password is tied to a specific account with a specific identity. An API key is a bearer credential, meaning possession of the key is the entirety of the authentication. There’s no second factor, no IP check by default, no identity verification. You have the key, you have the access.

And by default, most API keys don’t expire. GitGuardian found that 70% of secrets leaked in 2022 were still active in 2025, three years after the initial exposure. So the window of risk from a single leaked key isn’t hours or days, it’s potentially years if nobody catches it.


How they leak, in practice

The most common routes are fairly consistent across the audits I run and the research in this space. Hardcoding keys directly into source code and then pushing to a public repository is the most frequent and dangerous, because once a key is committed it stays in the version history even if you delete it later. Anyone who clones or forks the repository can retrieve it. Automated bots scan GitHub continuously for known credential patterns and can detect and begin exploiting a key within seconds of it being pushed.

Frontend code is the second major route. Any key that ends up in your JavaScript bundle is visible to anyone who opens DevTools, as I’ve covered in the bundle search step of the checklist. This isn’t theoretical, 7 out of 8 apps in my last Lovable audit had at least one credential in the bundle.

Beyond those two, GitGuardian’s 2025 research found that 28% of credential exposure incidents originated entirely outside source code, in collaboration tools like Slack, Jira, and Confluence. Pasting a key into a Slack message to debug something quickly, copying it into a ChatGPT prompt, dropping it into a Notion doc for a teammate, all of these are exposure routes that most founders aren’t thinking about at all.

There’s also a newer category worth knowing about. GitGuardian found 24,008 unique secrets in MCP-related configuration files on public GitHub in 2025, with over 2,000 confirmed valid. As AI agent tooling spreads, it’s introducing a new class of credential exposure that didn’t exist a couple of years ago.


What the automated attack chain actually looks like

This part is worth understanding concretely because it explains why speed of response matters so much if a key is ever exposed. The attack chain documented by security researchers looks like this: a bot identifies a known credential pattern in a public file. The token is automatically tested against the vendor’s API, which takes seconds. If it returns a valid response the key is confirmed live. The system then logs the vendor, the scope of access, and the permissions the key carries. From there it’s either exploited directly or sold.

The practical consequence of this is that “I’ll rotate it later” is not a viable response to a discovered exposure. One internal review found that the average time from detection to full rotation was 47 minutes when users revoked the key first, versus 4.2 hours when they started by investigating what happened before revoking. The order of operations matters: revoke first, investigate second.

For context on what this looks like at scale: in 2024 attackers breached the US Treasury Department using a leaked API key for BeyondTrust’s authentication platform. One exposed static credential bypassed what were reportedly millions of dollars in security investments and gave attackers direct access to Treasury systems.


The distinction between public and secret keys

This trips up a lot of founders and is worth being clear on. Not all API keys are created equal in terms of what’s acceptable to expose. Some keys are designed to be public facing, Stripe’s publishable key and Supabase’s anon key being the most common examples in the apps I audit. These are intended to appear in frontend code, and the security model is built around other controls (Supabase RLS, Stripe’s server-side verification) rather than the key itself being secret.

Secret keys are an entirely different matter. Stripe’s secret key, Supabase’s service_role key, your OpenAI API key, any key prefixed with sk- in your setup. These should never appear in client-side code under any circumstances. The distinction in Next.js is instructive here: variables prefixed with NEXT_PUBLIC_ are intentionally exposed to the browser, everything else is server-side only. Using NEXT_PUBLIC_ with a secret key is a critical misconfiguration that AI tools have been known to produce.

When doing a bundle search on your frontend code (the checklist covers how to do this), finding a public key is something to investigate in context. Finding a secret key is an immediate critical severity finding regardless of anything else in your setup.


What proper handling actually looks like

Environment variables are the baseline, not the ceiling. Keeping secrets out of source code and storing them in .env files is a necessary first step, but the .env file itself needs to be in your .gitignore and never committed. This is something I’ve seen violated more than I’d like to admit.

Beyond that, a proper secrets manager is the right approach at anything beyond the earliest stage. Tools like Doppler inject secrets into your application at runtime rather than storing them in files that can be accidentally committed or shared. They also give you audit logs, access controls, and the ability to rotate keys across multiple environments from one place, which matters considerably once you have more than one person touching the codebase.

On the key configuration side itself, two things are worth doing that most founders skip entirely. First, scope your keys to the minimum permissions actually required. Most API providers let you create keys with restricted access, and a key that can only read data does far less damage if compromised than one with full read/write access. Second, set expiration dates where the provider supports it. A key that expires in 90 days limits the window of exposure from any single leak considerably.

And if you ever find that a key has been exposed, the sequence is: revoke it immediately in the provider’s dashboard, generate a new one with minimal scope, update your environment variables, redeploy, and then pull 30 to 90 days of usage logs to audit for any unauthorized activity. The investigation happens after the key is gone, not before.

Back to blog

(function() { function applyMainRole() { var hero = document.getElementById('hero'); if (!hero) return false; var node = hero; while (node.parentElement && node.parentElement !== document.body) { node = node.parentElement; } if (node && node.parentElement === document.body) { node.setAttribute('role', 'main'); return true; } return false; } if (applyMainRole()) return; var attempts = 0; var interval = setInterval(function() { attempts++; if (applyMainRole() || attempts > 20) { clearInterval(interval); } }, 250); })();