Your Stripe live key is in your client bundle. Read this in the next 10 minutes.
sk_live_ appears in your JavaScript bundle, every visitor to your site can issue refunds, create charges, pull customer data, list saved payment methods, and disable your webhook endpoints — all using your Stripe account. The fix is five steps in roughly this order: roll the key, verify the new key works server-side, audit recent API logs for unfamiliar requests, file with Stripe if abuse is found, and prevent recurrence with restricted keys plus a CI gate. Don't read the rest of this article. Do step 1 first.
Stop reading. Do this:
curl -sL https://your-app.com/ | grep -oE 'sk_live_[A-Za-z0-9]{24,}'
If anything matched, your Stripe secret key is in your client bundle right now. Open dashboard.stripe.com/apikeys in another tab. Click "Roll key" next to your secret key. Confirm. Now read the rest.
What an attacker can do with a leaked sk_live_
The publishable key (pk_live_) is meant to be public — it's just a tag identifying your account. The secret key (sk_live_) is roughly equivalent to a logged-in admin session on dashboard.stripe.com:
- List, create, refund, and cancel charges on any of your customers
- List customer details, including saved payment methods
- Disable or modify your webhook endpoints (and re-enable them later, with logs scrubbed)
- Create disputes, issue full refunds even where you'd normally have refused
- List Connect accounts, transfer balances, modify payout schedules
- Run radar rules off, then issue any test charge they want
Every minute the leaked key is valid increases the risk. The first thing automated scrapers do when they find a Stripe live key is enumerate Connect accounts and try a test transfer. The second thing is open a refund loop.
Step 1 — Roll the key (90 seconds)
Stripe Dashboard → Developers → API keys → "Roll key" next to your live secret key.
The old key dies immediately. Every server, function, cron, or third-party service that was using it returns 401 until updated. This is correct. The breakage is how you find every place the old key lived.
Step 2 — Verify the new key works on your server (2 minutes)
Stripe gives you the new key once on screen. Copy it to your secret manager:
- Vercel / Netlify / Cloudflare Pages: project Settings → Environment Variables. Paste under the existing
STRIPE_SECRET_KEYname. Do not use a name with theNEXT_PUBLIC_,VITE_, orPUBLIC_prefix — that's how it ended up in the client in the first place. - Render / Fly.io / Railway: secrets / environment variables tab.
- Local dev:
.env.local. Confirm.env.localis in.gitignore.
Verify it works by hitting your own server-side endpoint that uses Stripe — a checkout creation or webhook receiver. If that returns 200, you're back in business.
Step 3 — Audit Stripe API logs (3 minutes)
Dashboard → Developers → Logs. Filter by date range covering the time the key was exposed. Look for:
- Requests from IP addresses you don't recognise (filter by
request.ip) POST /v1/refundsfrom outside your server's IPGET /v1/customerswith large pagination — bulk customer enumerationPOST /v1/payment_methodscreating cards on customers- Calls to Connect APIs (
/v1/accounts,/v1/transfers) if you don't use Connect - Webhook endpoint changes you didn't make
If you see anything alarming, take a screenshot of each suspicious entry. You'll need it for step 4.
Step 4 — File with Stripe if abuse is found (5 minutes)
If your audit shows unauthorised activity:
- Email [email protected] with subject "Compromised live secret key — abuse detected"
- Include: the key (rolled, no longer valid), the date range you're concerned about, screenshots from the API logs
- Stripe will reverse fraudulent charges and freeze the offending Connect account if it was used to launder the funds
You also have notification obligations under GDPR / CCPA / state breach laws if customer payment data was accessed. The right time to notify is when you discover it, not when you've fully scoped it.
Step 5 — Prevent recurrence
Three controls:
Use restricted keys, not the root secret key
Stripe supports restricted API keys with format rk_live_*. Each restricted key has a custom set of permissions. For most server-side use cases — creating customers, charges, subscriptions — a restricted key with read+write only on those resources is enough. If a restricted key leaks, the blast radius is bounded.
Generate a new rk_live_ with the minimum permissions your code needs. Use that for production. Reserve the root sk_live_ for one-off CLI work only.
Stripe's automatic key revocation
Stripe scans GitHub for leaked keys and rolls them automatically when found. This catches you only if the leak was a public commit. Bundle leaks (the kind we're discussing here) don't trigger Stripe's scan because the bundle isn't on GitHub. So this isn't enough on its own.
Add a CI gate
Run a check on every deploy. The simplest:
# In your CI:
DEPLOY_URL="${VERCEL_URL:-${DEPLOY_URL}}"
if curl -sL "$DEPLOY_URL" | grep -qE 'sk_live_[A-Za-z0-9]{24,}'; then
echo "FAIL: Stripe live secret key in deployed bundle"
exit 1
fi
Or use the vibecheck CLI to gate on every critical finding:
npx @vibecheck/cli "$DEPLOY_URL" --exit-on critical
Frequently asked
How did the key get into my bundle?
Almost always: an env-var with a NEXT_PUBLIC_, VITE_, or PUBLIC_ prefix. Or an AI builder that pasted the secret key into a React component because you said "wire up Stripe." Or a copy-paste from a tutorial that had the key inline as an example. Find the env var name; that's the smoking gun.
How long was it valid?
From the time you committed/deployed it. Stripe doesn't tell you when a key was first used — you have to reconstruct from API logs. Assume the worst case: the entire window from first-deploy to now.
Can I just use the publishable key (pk_live_) instead?
Yes — for any operation that goes through Stripe's checkout flow, Elements, or Payment Element. The pk_live_ key is designed for client use and only allows the operations Stripe meant to be client-safe (create payment methods, complete payment intents). Use it everywhere your client code touches Stripe. Move every other Stripe call to your server.
Should I rotate even if I haven't seen unauthorised activity?
Yes. Absence of evidence is not evidence of absence. Automated scrapers harvest GitHub and indexed pages on a 24-hour cycle; the key may already be compromised even if the abuse hasn't started yet. Rotate now.