← Writing

How an Unpatched Next.js RCE Leaked My OpenAI API Key and Drained My Credits

A critical React Server Components bug let an attacker run code on my server and steal secrets.

280

Running React 19 with the Next.js App Router? Stop and check your versions. A vulnerable app can let an unauthenticated attacker run code on your server. Once they are inside, your environment variables, API keys and cloud credentials are theirs to read.

I learned this the hard way.

It started as a billing problem. Then it became a security incident. Then I realised someone had been living inside my application.

It started with an OpenAI bill

My OpenAI usage had jumped. Not a little. A lot.

The strange part was that the usage didn't match anything my application does. There were thousands of video and audio translation requests, and my app performs none of those. No legitimate workload could explain the spike.

Someone, or something, was using my key. My first thought was the one every developer dreads:

I must have leaked my API key.

So I went through the usual suspects:

  • A .env file accidentally committed to GitHub
  • The key sitting in an old commit
  • A forgotten configuration file
  • A copy pasted somewhere I no longer remembered

I combed through environment files, configuration and Git history. Then I did the textbook thing: generated a new key, updated production, redeployed, and waited for the unauthorised usage to stop.

It didn't.

The new key was stolen too

This is where the incident changed shape completely.

The new key had never been committed to GitHub. It wasn't published anywhere. It wasn't in any old history. It existed in exactly one place: my production environment.

Within hours, the attacker was using it.

How did someone get a secret that had never left my production server?

Only one answer made sense. They weren't stealing my key from GitHub. They were stealing it from the server. And if they could read my environment after a rotation, rotating again was pointless. Something had live access to the running application.

Then I read the logs

I stopped staring at OpenAI usage in isolation and started correlating every log source I had.

  • CDN logs showed repeated POST requests hitting /. They didn't look like real users clicking through the app.
  • Runtime logs, at roughly the same times, showed something far worse: shell commands being executed. Several were hunting for .env and other configuration files.

That was the clue. Someone wasn't just calling my API. Someone was running code inside my application.

Once an attacker can execute arbitrary code on your server, reading an environment variable is trivial. Everything the app needs to do its job is sitting right there:

OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxx
DATABASE_URL=postgresql://...
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...
JWT_SECRET=...

Those values exist so your code can use them. Code execution means their code can use them too.

The vulnerability: React2Shell

The door was CVE-2025-55182, better known as React2Shell. The React team disclosed it on 3 December 2025 with the maximum CVSS score of 10.0.

The bug lived in how React decodes payloads sent to Server Function endpoints. An unauthenticated attacker could craft one HTTP request that, once deserialised, ran code on the server. According to the React advisory, it hit versions 19.0, 19.1.0, 19.1.1 and 19.2.0 of the react-server-dom-* packages, and frameworks that bundle them, including Next.js. Even apps that never define a Server Function could be exposed if they support Server Components.

If you use the Next.js App Router, you use React Server Components. That was me.

Attack chain: a crafted POST to / hits the RSC payload decoder, triggers code execution, the attacker reads env and files, exfiltrates secrets, and reaches the database, cloud account and OpenAI, where credits are consumed

The malformed request was never the goal. It was the door. What happens after an attacker walks through depends entirely on what your application can reach.

Why my OpenAI key was the perfect target

My backend holds an OpenAI key because it needs to call OpenAI. That is completely normal. Before the compromise, the picture looked like this:

My application ──(API key)──▶ OpenAI

After it, it looked like this:

Attacker ──(my stolen API key)──▶ OpenAI

OpenAI cannot tell the difference. The key is the identity. Whoever presents it gets served, and the bill lands on the account that owns it.

That is why the usage looked so alien. Once a credential is stolen, the attacker decides what it's used for: their own apps, automation, experiments, bulk media jobs. You pay for all of it.

The attacker wasn't stealing my credits directly. They were stealing the credential that had permission to spend them.

Treat API keys like passwords. A leaked key isn't just a data leak. It's an open tab at a bar, in your name.

Rotating the key didn't save me

This was the biggest lesson of the whole incident. My first instinct was: the key is compromised, rotate it. Correct, but dangerously incomplete. The vulnerability was still there, so every fresh key went straight into the attacker's hands.

Key rotation loop: usage spike, rotate the key, new key in env, RCE still open so the attacker reads it, abuse resumes within hours, repeat. The fix: patch and redeploy first, then rotate and revoke

I had changed the secret. I hadn't closed the door. When you suspect a server compromise, fix whatever exposed the secret first, then rotate. Otherwise you are just restocking the shelf the thief is emptying.

What else they could have taken

The OpenAI key was only one secret, and that's what made this serious. A typical production environment holds far more:

OPENAI_API_KEY        DATABASE_URL         JWT_SECRET
AWS_ACCESS_KEY_ID     STRIPE_SECRET_KEY    SMTP_PASSWORD
AWS_SECRET_ACCESS_KEY REDIS_URL            OAUTH_CLIENT_SECRET

With arbitrary code execution, the question isn't "can they steal my OpenAI key?" It's "what does my application have access to?" That is your blast radius.

Thankfully, least privilege helped

One thing worked in my favour. The cloud credentials my app used were not administrator credentials. They were deliberately scoped to specific resources and operations, so even exposed, they couldn't take over the account.

Blast radius comparison: with administrator credentials a compromised app reaches the entire cloud environment; with a restricted service identity it reaches only specific resources and actions

Neither side is good news. But one is a bad week, and the other is the end of your company. Least privilege doesn't prevent breaches. It stops a breach becoming a catastrophe.

The fix wasn't just "upgrade Next.js"

Once I understood what had happened, the response got much bigger than a dependency bump. The order matters. Rotate before you patch and you're just feeding the attacker fresh secrets.

  1. Patch the vulnerable application. Upgrade Next.js and the React/RSC packages to a currently supported security release.
  2. Redeploy, and verify. Confirm the patched build is what's actually running in production, not just what's in package.json.
  3. Rotate every potentially exposed secret. Not just the one that tipped you off:
    • OpenAI and other third-party API keys
    • Database credentials
    • Cloud credentials
    • JWT signing secrets
    • OAuth client secrets
  4. Revoke the old credentials. Rotation doesn't always mean the old value stops working. Check that it's dead.
  5. Investigate the logs. Look for:
    • Unexpected POST requests and suspicious source IPs
    • Shell commands and unknown processes
    • Unusual outbound connections
    • Cloud API calls, new users or new resources
    • Unexpected database activity
  6. Check billing everywhere. For me, the OpenAI bill was the alarm bell. It shouldn't have been the only one.

Check your versions, then cap your keys

Don't guess whether you're affected. Check the real dependency tree:

npm ls next react react-dom react-server-dom-webpack

Then inspect the lockfile and, more importantly, what is actually deployed.

And don't trust an old post that says "upgrade to X.Y.Z and you're safe". React2Shell was only the first RSC bug. Follow-up research surfaced denial-of-service and source-code-exposure flaws (CVE-2025-55183, CVE-2025-55184, CVE-2025-67779), and another DoS (CVE-2026-23864) landed in January 2026. As of the React advisory's last update, the minimum safe Next.js per release line was:

If you're on Upgrade to at least
13.3.x – 14.x 14.2.35
15.0.x 15.0.8
15.1.x 15.1.12
15.2.x 15.2.9
15.3.x 15.3.9
15.4.x 15.4.11
15.5.x 15.5.10
16.0.x 16.0.11
16.1.x 16.1.5

The goal is a currently supported, fully patched release, not the first version that fixed the original bug. Check the advisory again before you deploy; this table will age.

Assume one day a key will be stolen

This lesson has nothing to do with React. Design every credential so that theft is survivable:

  • Set usage limits and hard spending caps where the provider supports them
  • Configure billing alerts that reach your phone, not just your inbox
  • Monitor for unusual traffic patterns and endpoints
  • Use separate keys per environment and per application
  • Never share a production key across apps
  • Rotate immediately when compromise is suspected
  • Give cloud identities only the permissions they actually need

You want the worst case to be "someone used my key for an hour", not "someone had unlimited access to my entire infrastructure".

Your application can be the leak

Before this incident, "API key leak" meant GitHub to me. Committed .env files. Public repos. Secret scanning. Those still matter.

But there's a far more uncomfortable path. Your application itself can become the secret leak. You can do everything right with Git, keep .env out of version control, and load environment variables by the book. If an attacker can run code inside your production process, none of those values are secret any more.

The attacker never had to find my key online. They made my own server hand it over.

So when the next critical RCE drops, don't stop at npm update. Ask the bigger question:

If someone had code execution on my server, what could they have accessed?

A vulnerability in a framework isn't just a framework problem. In production, it becomes your credentials, your infrastructure, your data and your money.

  • Patch quickly, and keep dependencies current
  • Treat API keys like credentials, because they are
  • Use least privilege everywhere
  • Set spending caps and usage alerts

And don't wait for the bill to tell you something is wrong. Check your servers first.


Sources

Systems
280
More from Kobby Owen
How do you stop an AI from making things up? RAG Basics
RAG Systems, Explained Like You’re 7
30 Aug 2026 · 10 min read
What actually breaks in a RAG system
Examines structural failures in retrieval-augmented generation systems with fixes from real world examples
30 Aug 2026 · 21 min read
Your rate limiter says 100 req/s. Your users get 400.
Explains how in-process and Redis-backed rate limiters differ and why per-instance limits multiply when scaling horizontally.
27 Aug 2026 · 7 min read