Security Guides

An API key leaked from AI-written code and was used for fraudulent charges: the cause was an unpatched CVSS 10.0 vulnerability

An app built with AI help had its API key stolen and ran up fraudulent charges. The real cause was an unpatched CVSS 10.0 RCE. How it was found and prevented.

Published 2026-06-07 Updated 2026-10-06 14 min read
On this page

A case that indie developers without a security background commonly run into, with every identifying detail removed and turned into a lesson. There are no attack reproduction steps here. The goal is defense, so the same mistake doesn't happen to you.

Case file — INCIDENT FILE
Class
API key leak / fraudulent billing
Severity
Critical (a published CVSS 10.0 abused)
Root cause
A pre-auth RCE in the web framework left unpatched for months
Leak scope
Every secret in the environment
Execution scope
Stayed inside an unprivileged container (host root unharmed)
Permanent fix
①upgrade to patched version ②rotate all keys ③machine CVE monitoring
10.0
CVSS / worst class
months
how long unpatched
all env
assumed leak scope
monitoring
the most effective prevention

“Stopped the charges” ≠ “handled”

Stopping the bill is a temporary measure. Closing the leak path is a separate task. You're only done when you've done both.

What happened (timeline)

  1. Day 0 — shipped & running

    An app built with AI's help was shipped and running in production.
  2. One day — the bill spikes

    The cloud-AI bill suddenly jumps. Bulk jobs on a model that would never be used, doing work that wasn't theirs.
  3. Investigation — third-party abuse

    "Our app ran wild" doesn't explain it. A third party was running it with a stolen key.
  4. Tracing — investigating the leak path

    "So where did the key leak from?" This is where the actual investigation started.

Why it wasn't caught at first

In the order the mistakes were made — because the mistakes are the lesson.

1

Decided on a cause too early

First assumption: "our own fallback code malfunctioned." But the data (what was actually processed) made clear it couldn't be that.
→ Before deciding "my fault" or "their fault," look at the data first.
2

A clean grep almost ended the search

All code, git history, linked repos searched → no plaintext keys anywhere. Nearly gave up as "untraceable." Looking only at files, the runtime leak was missed.
3

Hiding the symptom felt like fixing it

Masking the symptom at the proxy nearly felt like done. But masking leaves the RCE in place. It stays exploitable until the cause is fixed.

A clean grep is not the all-clear

Even with no key in any file, a vulnerability can pull environment variables out of a running process. Leaks happen at runtime (RCE, HTTP headers), not just in files. See What is RCE.

The actual cause: a published CVSS 10.0 left unpatched

A strange signature in old access logs quickly matched threat intelligence. The web framework version range in use had a published pre-auth RCE (CVSS 10.0) that was already being exploited in the wild — and it had run unpatched for months.

  • This wasn't a passive bug — it was an attacker executing code on the server and exfiltrating the environment.
  • One thing limited the damage: execution stayed inside an unprivileged container (host root wasn't taken). The breach review found no persistent backdoor, miner, or C2 — the confirmed impact was secret theft.
  • Because the internal DB was reachable, the response assumed the DB contents had leaked too.
Visible symptom: a surprise, unrecognized bill
this is all the owner can see
↑ stolen keys are resold / abused later
the whole env (every secret) is exfiltrated
↑ attacker runs arbitrary code on the live server
cause: a published CVSS 10.0 left unpatched (pre-auth RCE)
The surprise bill is only the visible symptom. Underneath it is the cause: a published CVSS 10.0 left unpatched.

Lesson: when something behaves strangely, suspect a known CVE first. Don't assume it's your own bug.

Narrow down the leak: where did that key live?

When grep finds nothing, change how you search. List where the abused key was written down, and the candidate leak paths shrink. If the abused key only ever existed in the production server's environment variables, for example, suspect the production server or the hosting provider, not Git or the browser.

Where it leaksCommon causeHow to check your own setupHow to close it
Git history / public reposCommitting .env, hardcoding keysScan the full history with gitleaks git. Check GitHub secret scanning alertsRevoke first, then block it before commit
JavaScript shipped to the browserA key in a NEXT_PUBLIC_ variable, calling the API straight from the browserSearch the build output for the key's prefixMove the API call to server-side code
A public web folder.env or backups placed in the public folderOpen /.env on your own site and confirm it returns 404 or 403Move secret files outside the public folder
Logs / error pagesShipping with debug output on, logging request headers as-isCheck production debug settings and whether Authorization and similar appear in logsTurn off debug output and mask secrets in logs
The running serverA published vulnerability (such as an RCE) left unpatchedCompare the version actually running with the fixed version in the advisoryUpgrade, then replace every secret in the environment
The hosting providerA breach on the provider's sideCheck the provider's security bulletins and email noticesStore secrets so values can't be viewed later, and rotate
Your development machineA malicious package or extensionCheck whether package or extension versions you installed match a published compromiseRemove that version and replace secrets that were on the machine

This case was the "running server" row. The bottom four rows are paths that leave no trace in your code or Git. OWASP's Secrets Management Cheat Sheet also warns that environment variables are often readable by other processes in the same environment and can end up in logs or dumps. So when one key is abused, treat every secret stored in the same place as leaked.

Separate keys make the investigation faster

If you use a different key per service and per environment (production vs development), the abused key alone tells you which row of the table you're in. Reuse one key everywhere and you have to suspect every location. How to split keys is covered in When a stolen API key gets your OpenAI account banned.

Count vulnerabilities by the version actually running

Checking the other dependencies, "8 more vulnerable dependencies" was reported. That was also wrong. They'd been counted by the lower bound in package.json (the ^ caret range). The truly dangerous ones were only the 2 pinned dependencies left behind.

Check your own running versions

See the versions actually resolved, in the lockfile or the running container.

# npm: see what's actually installed
npm ls next react react-dom
 
# inside a running container
docker exec <container> npm ls <package>

The numbers here are the versions actually running, not the ^ in package.json.

How to check your own setup

Whether you've just noticed a strange bill or nothing has happened yet, you can check in this order. Every step only looks at your own account and server.

1

See which key was used, and when

In the AI API dashboard, break usage and cost down by project (a Workspace, on Anthropic) and by day. Usage on models you never use, or at hours when nothing of yours runs, means that key leaked. Then start from the table row for wherever that key was stored.
2

Check the files shipped to the browser

In Next.js, the values of environment variables starting with NEXT_PUBLIC_ are inlined at build time into the JavaScript sent to the browser. After a build, search the output for your key's prefix, for example grep -rl "sk-" .next/static. Anything found there was readable by anyone.
3

Confirm secret files aren't reachable from the public folder

Run curl -sI https://example.com/.env against your own site (replace example.com with your domain) and confirm the status on the first line is not 200 (it should be 404 or 403). More in auditing secret files in public directories.
4

Compare the production version with the advisory

As in the box above, run npm ls inside the production container to see the version. osv-scanner scan -L pnpm-lock.yaml checks whether your lockfile includes versions with known vulnerabilities.
5

Check logs for secrets

Run gitleaks dir --redact on the folder where your app's logs are stored to find keys written into logs. --redact keeps the found values themselves off the screen.
6

Check limits and alerts

OpenAI lets you set spend alerts and a monthly hard spend limit on the limits page of its dashboard. Anthropic lets you set a spend limit per Workspace. Make sure you don't only have alerts that never stop anything.

What was done first (reproducible steps)

1

Revoke the abused key immediately

Stops the damage, but it is only part of the job.
2

Upgrade the framework to the patched version

Actually close the RCE. After upgrading, confirm the leak signature disappears in the logs.
3

Rotate every secret

Assume-leaked. Order by "abusable from anywhere first": object-storage keys → session-signing key → API keys → DB credentials.
4

Breach review

Check for persistence, rogue cron, outbound C2 (none here).
5

Account-side defense

Change the password, enable MFA, check for unfamiliar API keys.

Prevention: monitor CVEs automatically

Boiled down, this incident was "a human overlooked a published CVSS 10.0." Manual checks always miss things. Automated monitoring catches them.

Dependency scanning you can start today

Free to start — one step in CI.

# OSV scanner (Google) checks your lockfile
osv-scanner scan -L pnpm-lock.yaml
 
# On GitHub, enable Dependabot (repo Settings → Security)

This site itself, per this lesson, monitors its own dependencies for CVEs — we practice what we recommend.

Common mistakes and how to fix them

  • Putting a new key on a server that's still open: the same path steals the new key too. Stop the abused key right away, fix the vulnerability, and then finish replacing the remaining secrets.
  • Treating a budget alert as a cap: Google Cloud states that an alerts-only budget doesn't automatically stop usage or spending. If you want it stopped, add a limit that blocks usage, such as OpenAI's monthly hard limit or an Anthropic Workspace spend limit.
  • Putting an AI API key in a browser or mobile app: any value that reaches the user's device can be pulled out. Call the AI API from your server, and have the browser call your server.
  • Deleting it from files or Git history and stopping there: anyone who already copied it can still use it. Revoke first, clean up history later (see why leaked GitHub credentials stay valid).
  • Using the same key in development and production: you can't tell which side leaked, so you have to stop both. Issue a separate key per environment.

A public example: environment variables leaked from the provider's side

Leaks that no file search can find also happen outside your own server. On April 19, 2026, Vercel disclosed unauthorized access to its internal systems. According to the company, a compromise of a third-party AI tool used by an employee led to the theft of some customers' environment variables that were not marked "sensitive" (the setting that makes a value unviewable after saving).

Vercel advised customers to review and rotate environment variables not marked sensitive, and to check activity logs and recent deployments. It also stated that deleting projects or the account is not enough to remove the risk.

Customers in that situation could search their code and Git all day and find no trace of a leak. The action is the same as in this case: replace every secret stored in that place. The steps are in the Vercel security incident (April 2026).

Lessons

Common mistakes

  • Calling it done at "stopped the charges"
  • Relaxing because grep is clean
  • Masking the symptom at a proxy and feeling fixed
  • Counting vulnerabilities by the package.json floor
  • Rotating only the one confirmed-abused key

Correct defense

  • Treat stopping the charges and closing the leak path as two separate jobs, and do both
  • Suspect runtime leaks (RCE, headers) too
  • Update the framework that caused it
  • Judge by the version actually running
  • Replace the whole env if it leaks

To sum up: the bill spike was a symptom, and the cause was a CVSS 10.0 RCE left unpatched. The correct cause was not found on the first try; it was found by being wrong several times and checking again.

Sources

FAQ

QIf an API key leaks, is it enough to revoke just the one key that was abused?
A

No. When the leak path is at runtime (an RCE or a header leak), assume every secret in the environment leaked at once, and rotate them all. The key you saw abused is only a small part of what leaked.

QIf grep across my code and git finds no keys, am I safe?
A

Not necessarily. Even with no key in any file, a framework vulnerability can exfiltrate environment variables straight from the running process. Leaks happen at runtime too, not just in files.

QWhat stops this from happening again, most reliably?
A

Machine-monitoring your dependencies for CVEs. The cause here was a human overlooking a published CVSS 10.0 for months; Dependabot or osv-scanner catches that automatically.

QMy AI API bill suddenly jumped. What should I do first?
A

First, disable the abused key in the provider's dashboard to stop the damage. Then narrow down the leak path from where that key was stored, fix the vulnerability or misconfiguration, and only then replace every secret that lived in the same place. Revoking the key alone leaves the leak path open.

QHow do I find out which key leaked?
A

In the AI API dashboard, break usage and cost down by project (a Workspace, on Anthropic). The key behind usage on models or at hours you never use is the leaked one. If you keep separate keys per service and environment, where that key was stored also tells you the leak path.

QIf I set a budget alert, will fraudulent charges stop?
A

Not necessarily. Google Cloud, for example, states that an alerts-only budget does not automatically cap usage or spending. To actually stop usage, set a limit that blocks it, such as OpenAI's monthly hard spend limit or a per-Workspace spend limit on Anthropic.