Is your Stripe secret key in your JavaScript bundle right now?
Open your live site, press F12, and search the loaded JavaScript for sk_live_. Do you know what comes back? Most people who wired up Stripe months ago genuinely don't. Payments work, money arrives, and nobody has looked at the integration since.
But Stripe hands you two keys that look almost identical. One is meant to be public. The other can move your money. They get copied from the same dashboard page, one above the other, so it is genuinely easy to paste the wrong one into a file that gets bundled and served to every visitor. If you did, it's sitting in your bundle right now, readable by anyone who opens their browser's developer tools.
How it gets there
Stripe gives you a pair of keys. The publishable key starts with pk_live_ and is designed to live in your frontend. It can do almost nothing on its own, which is the point. The secret key starts with sk_live_ and can do everything: create charges, issue refunds, pull your customer list, read payout details. It is meant to stay on your server and never leave it.
The trouble is how close they sit. They're on the same dashboard screen, one above the other. When you're moving fast you copy a key, paste it into a config file or a component, and keep going. If that file is part of your client bundle, the secret key gets minified, packed in with everything else, and served to the public. By then it's just one more string in a wall of compressed JavaScript, and nobody reviews that wall.
Frameworks make it worse in a quiet way. A NEXT_PUBLIC_ prefix in Next.js, a VITE_ prefix in Vite, anything you mark as a build-time public variable gets inlined into the bundle on purpose. Name your secret key with the wrong prefix and the framework will helpfully ship it for you, no warning.
What a stranger does with it
This is the part people underestimate. Finding the key is not hard, and using it is not hard either.
Anyone can open your site, press F12, and search the loaded JavaScript for sk_live_. It takes seconds. There are also automated crawlers that do nothing but scan public bundles for key patterns all day, so you don't even need a person to find it. Once they have the string, it is a working credential. No password, no second factor, nothing else to defeat. The key is the access.
With a live secret key, here is what's on the table:
- Issue refunds, including to themselves, draining whatever balance you're holding.
- Create charges against saved payment methods.
- Read your full customer list: names, emails, billing details, payment history.
- Pull metadata about your business, your payouts, your volume.
You will not get an alert when this starts. Stripe sees valid API calls signed with a valid key. As far as it knows, that's you. The first sign of trouble is usually your balance, or a customer asking why they were charged. By then the key has been live and public for as long as that bundle has been deployed, which might be months.
A test key, the sk_test_ kind, is less catastrophic but still wrong. It exposes your test data and signals exactly how your integration is wired, which is reconnaissance an attacker is happy to have.
So which key is in your bundle?
You can check this yourself in under two minutes, no tools required.
- Open your site in Chrome or Firefox.
- Open developer tools (F12, or right-click and Inspect).
- Go to the Sources tab and open the search across all loaded files (Ctrl+Shift+F, or Cmd+Option+F on a Mac).
- Search for
sk_live_. Then search forsk_test_.
If either returns a match, that key is public. Treat it as compromised from this moment, because you have no way to know who has already read it.
If the only Stripe key you find starts with pk_live_ or pk_test_, you're fine. Those belong in the frontend and are safe there by design. The prefix is the whole game. pk_ is public on purpose; sk_ and rk_ are secrets meant to stay on your server.
How to fix it
Two steps, in this order, and the order matters.
Rotate the key first. In the Stripe dashboard, roll the exposed secret key so the old one stops working. Do this before anything else. Cleaning the code without rotating leaves the leaked key valid for as long as the old bundle is cached anywhere, and bundles get cached in a lot of places. The key is burned the moment it ships publicly, so the only safe assumption is that someone has it.
Then move the key off the client. The call that uses your secret key belongs in a server route: a Next.js route handler, an API endpoint, a serverless function, whatever your stack uses. The browser talks to your server, and your server talks to Stripe holding the secret key. Store the key in a server-side environment variable, not one with a public prefix, and confirm it never appears in the built output.
// components/Checkout.tsx - shipped to the browser
const stripe = new Stripe("sk_live_51HxQp••••Yz");
await stripe.charges.create({ amount, source });The publishable pk_ key is the only Stripe key that should reach the browser.
Find it before someone else does.
Paste your domain. The grade and issue count are free, and you'll see in a couple of minutes exactly what's reachable from outside.