← Back to blog
SecurityScanaris Team · 6 min read · October 04, 2026

The secrets your Cursor or Lovable app is leaking in the browser

When you ask an AI to "connect my app to Stripe, OpenAI or Supabase", it often drops the key straight into the frontend code. It works on the first try, which is exactly why nobody reviews it. The problem is simple and absolute: everything that reaches the browser is public. There is no way to hide a string the browser needs to read. This guide explains why it happens, how an attacker finds it in a minute, which keys are dangerous and which are not, how to search for them yourself and what to do if one has already leaked.

Why the key ends up in the browser

There are three usual paths. The first is the public prefix on environment variables: in Next.js anything starting with NEXT_PUBLIC_ is baked into the client JavaScript, just like VITE_ in Vite, NUXT_PUBLIC_ in Nuxt or REACT_APP_ in Create React App. The AI sees that "the variable is undefined" and adds the prefix so it arrives. The second is the direct call: the assistant writes fetch('https://api.openai.com/...') inside a UI component with the key in the header, because that is the shortest example it knows. The third is a shared constants file, imported by both server and client, where someone left the key "temporarily".

In all three cases the result is the same: the key travels in a .js file anyone can download, and it stays in CDN caches, in your users' browser history and in the archives of the crawlers that scan the internet looking for exactly that.

How an attacker sees it in 60 seconds

  • DevTools. Open your site, press F12, go to the Sources tab, press Ctrl+Shift+F to search across all files and type sk_live, sk-ant, AKIA, service_role or -----BEGIN. If it shows up, they already have it.
  • View source. Right-click, view page source, open each linked .js file. Bundles are minified but not encrypted; a text search is enough.
  • Source maps. If you publish the .map files, besides the code they see the original variable names and comments, including the // TODO: move to server.
  • Automated scanners. Bots crawl new domains downloading their JavaScript and testing every key pattern against the matching provider. An AWS key leaked in a bundle gets used in minutes, not days.

Which keys are dangerous and what happens when they leak

  • Stripe sk_live_. Creates charges and refunds and reads customer data. Real money.
  • OpenAI, Anthropic and similar (sk-…). Whoever holds it bills your account. Four-figure invoices from a key in a public repository are a story that keeps repeating.
  • AWS AKIA… and Google Cloud. Depending on permissions: buckets, databases and spinning up machines to mine at your expense.
  • Supabase service_role. Bypasses Row Level Security entirely: full read and write on your database. The most serious of all in vibe-coded apps.
  • Firebase service accounts (JSON). Equivalent to service_role: full control of the project.
  • Email and SMS providers (Resend, SendGrid, Twilio). Sending mail and messages in your name, that is, phishing with your domain.
  • Database connection strings with user and password, and JWT secrets: with the secret, anyone can sign sessions for any user.
  • Access tokens stored in localStorage. Not your keys but your users', and any XSS reads and steals them.

Which keys may go to the client

Not every key is a secret. Some are designed to live in the browser, as long as what sits behind them is configured properly:

  • Supabase's anon or publishable key, protected by the RLS policies on each table.
  • Stripe's publishable key (pk_live_), which only starts payments, never charges them.
  • The Firebase config (apiKey, projectId…), protected by security rules and, better, by App Check.
  • A Google Maps key restricted by domain from the Google console.
  • Analytics identifiers and third-party widget IDs.

The rule to tell them apart: a key may go to the client if the provider explicitly calls it public and if, even holding it, a stranger cannot do anything you do not want. If in doubt, it is secret.

How to search for them yourself

In the built bundle, search for known patterns. With Next.js or Vite, after building:

grep -rEo "sk_live_[0-9a-zA-Z]+|sk-ant-[0-9a-zA-Z-]+|AKIA[0-9A-Z]{16}|service_role|eyJhbGciOi[0-9a-zA-Z._-]+" .next/static dist 2>/dev/null | head

In the repository, before it reaches the bundle, tools like gitleaks or trufflehog walk the entire history, not just the latest commit. And in your hosting dashboard, review which variables carry a public prefix: every one of them is sent to the browser.

Three places people do not look: components marked 'use client' that import a shared config file; URL parameters, where a token sometimes travels and ends up in logs and history; and public GitHub repositories, where the key may have sat for months even if it is no longer in the current code.

How to fix it: the key stays on the server

The correct pattern is that the browser talks to your server and your server talks to the provider. An endpoint of your own receives the request, checks the user's session, applies a usage limit and makes the call with the key read from an environment variable without a public prefix. In Next.js:

// app/api/summarize/route.ts  (server only)
import { getSession } from '@/lib/auth'

export async function POST(req: Request) {
  const session = await getSession(req)
  if (!session) return new Response('Unauthorized', { status: 401 })

  const { text } = await req.json()
  if (typeof text !== 'string' || text.length > 5000) {
    return new Response('Bad request', { status: 400 })
  }

  const r = await fetch('https://api.openai.com/v1/chat/completions', {
    method: 'POST',
    headers: {
      Authorization: 'Bearer ' + process.env.OPENAI_API_KEY,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ model: 'gpt-4o-mini', messages: [{ role: 'user', content: text }] }),
  })
  return new Response(r.body, { headers: { 'Content-Type': 'application/json' } })
}

With Supabase, same idea: operations that need service_role go in an Edge Function or your backend, never in the client. With Firebase, in a Cloud Function. And in every case the environment variable is called OPENAI_API_KEY, not NEXT_PUBLIC_OPENAI_API_KEY.

For session tokens, prefer HttpOnly cookies over localStorage: an HttpOnly cookie cannot be read from JavaScript, so an XSS cannot steal it. If your library stores the session in localStorage by default, as the Supabase browser client does, compensate with a strict CSP or use its server-side variant with cookies.

If it has already leaked: rotate, do not delete

Removing the key from the code achieves nothing: it is already in caches, in git history and, probably, in someone's hands. The right order is:

  • Create a new key at the provider and deploy it on the server.
  • Revoke the old one immediately after. In Stripe, OpenAI and AWS it is one click in the dashboard.
  • Review usage: billing, API logs and charges from the last few days. If there was abuse, open a ticket with the provider; they sometimes waive the charges.
  • Set spending limits and alerts at every provider that allows it, so next time hurts less.
  • If the key gave access to users' personal data, assess whether you must notify under the GDPR.

Cleaning git history with tools like BFG or git filter-repo is good hygiene, but it does not replace rotation.

How Scanaris checks it

Scanaris downloads the JavaScript your site serves, just as an attacker would, and searches for key patterns of the usual providers, JWT tokens, webhook URLs and connection strings. It also warns if your source maps are reachable and if sensitive files are within reach. Every finding comes with a prompt to paste into your AI with the exact fix: move the call to the server and rotate the key. Turn on monitoring and it tells you when a new vibe coding session puts a key back in the client. It is part of the launch checklist and of the security guide for AI-built apps.

Frequently asked questions

Is the Supabase anon key a secret?

No. It is meant for the browser and the RLS of each table protects it. What is secret is service_role. If your RLS is wrong, the problem is not the anon key but the policies.

What if the key is restricted by domain?

Referer restrictions (as in Google Maps) reduce abuse from other websites, but not from scripts that forge the header. They are fine for low-risk keys, not for keys that spend money or read data.

Is deleting it from the repository enough?

No. A key that has been in a public bundle or a public commit is compromised. Always rotate it, then clean the history if you like.

How does your site do?

Run Scanaris and check it in 30 seconds, free.

Scan my site

Related checks

Security guides for your stack