Blog / Secret Leaks

Search Your Own Bundle for sk- Before Someone Else Does

· Fortus Team · 4 min read

Imagine you ship on a Friday afternoon. The AI assistant wired the checkout and the chat feature straight from the frontend because that was the simplest working architecture, and everything works. What you do not realize is that your secret Stripe key and your model provider key just shipped inside the JavaScript bundle every visitor downloads. Anyone who opens DevTools and searches can read them. This is the most common secret leak in quickly built apps, and it is also the easiest to check. Anything in the browser is public by definition: bundled JavaScript, HTML source, client components, and any environment variable with a public prefix that gets inlined at build time. If your frontend calls a provider endpoint directly with the key inline or interpolated from a client-visible variable, the key travels with the page. Picture a scenario I keep seeing: a builder who assumes the key is safe because it lives in an env file, not realizing the build step pasted that value into the shipped bundle. The consequences are concrete. Bots scrape public repos and live bundles for key patterns within hours. A payment key can be used to create charges or refunds depending on its scope. A model key becomes someone else's free inference budget on your bill. And rotating the key after launch does not undo whatever was already copied. The check below takes about a minute and tells you where you stand.

Why the frontend keeps eating your keys

AI-generated code defaults to the shortest path between the button and the provider. A chat box that calls the model API directly from a client component works immediately, no proxy route needed. A checkout button that uses the secret key inline avoids the extra step of building a server endpoint. Each shortcut feels harmless in isolation because the key is "just a string in my code." The build pipeline makes it worse. Public-prefixed variables are designed to be embedded in client code — that is their documented purpose. So a variable holding a publishable key and one holding a secret look identical in the source, and only the prefix decides which one ships. Add a Supabase client initialized with the service-role key instead of the anon key in a client-reachable file, and your row-level policies stop mattering for anyone reading the bundle. None of this shows up as an error. The app works perfectly while the keys sit in the open.

Run the 60-second self-test on your own app

Here is a concrete self-test you can run manually on your own deployed app right now. Open your app in the browser, open DevTools, and go to the Network tab. Trigger the AI feature or the payment flow, then use Ctrl+F or the search box to look for key prefixes: sk-ant-, sk-, sk_live_, sk_test_, rk_live_, AKIA, AIza, xoxb-, xoxp-, ghp_, gho_, and SUPABASE_SERVICE_ROLE. Then check the page source and the loaded JavaScript files for the same patterns. Next, confirm the call path. For each match, ask: does this request go from the browser straight to the provider, or does it hit your own backend route first? Open the source file that makes the call. If a client component imports a provider SDK and passes the key, or if the Supabase client in client-reachable code uses the service-role key, you have found the leak. Write down the file and line for each one. If every third-party call goes through your own API route or serverless function and the frontend only carries anon or publishable keys, you pass this round.

The fix is a proxy, not a hiding place

There is no way to hide a secret in client code — obfuscation, splitting the string, or fetching it from another public endpoint only slows down a reader by minutes. The fix is architectural: move every provider call behind your own backend route. The frontend calls your API route with the user's session, your server reads the secret from a server-only environment variable, calls the provider, and returns the result. Concretely: create a route such as your app router's API directory or a serverless function per provider operation, store keys in server-side variables only, and switch the frontend Supabase client to the anon key. Keep publishable keys publishable and secret keys server-side, and never reference a secret from a client component, a public directory asset, or a public-prefixed variable. Then re-run the bundle search to verify: grep your client-reachable directories for the key patterns above and confirm each remaining match is an anon or publishable key, and confirm every provider call originates from server code.

Where a source-file scanner fits in

A scanner that reads your repo files can automate the tedious half of this. It can grep client-reachable files for key-pattern literals, flag direct fetch or SDK calls from client components to provider endpoints, catch a service-role key used where the anon key belongs, and recognize the handled pattern — calls routed through backend routes with the key held server-side — so it does not raise false alarms on a correct setup. What it cannot do is watch your live network traffic or tell you whether an already-exposed key was harvested. Those answers live outside the repo, in DevTools and in your provider dashboard. Treat the scanner report as the file-evidence map, run the manual DevTools check to confirm what actually ships, and if anything was ever exposed, rotate the key at the provider first — removing it from code fixes nothing until the old value is dead.

Automated Code Review

Keep your secret keys out of client bundles

Fortus flags provider calls made from frontend components and service-role keys exposed in public directories before your code ships to visitors.

or learn more about Fortus →