How this check works
Scanaris loads your home page, collects the JavaScript files it links from your own domain and reads them looking for patterns of known credentials: Stripe secret and restricted keys (sk_live_, rk_live_), AWS access key IDs (AKIA…), Google API keys (AIza…), OpenAI keys, GitHub tokens (ghp_…) and PEM private keys. It also decodes any JWT it finds and, if its payload carries role: service_role, flags it as an exposed Supabase service key, which is critical. Public keys (Stripe's pk_live_, Supabase's anon or publishable, the Firebase config) are deliberately excluded: they are designed to ship in the browser.
What an attacker does with this
Exactly what the scanner does, but at scale: there are bots that download the JavaScript of new domains as soon as they appear in certificate logs and try every key they find. A Stripe secret key allows creating refunds or reading customers; an OpenAI key, spending your quota; a Supabase service_role, reading and deleting your whole database while bypassing the policies. Nothing needs to be "hacked": the file is public by definition, every visitor receives it.
How a key ends up in the bundle
Almost always through an environment variable with the public prefix: NEXT_PUBLIC_ in Next.js, VITE_ in Vite, NUXT_PUBLIC_ in Nuxt. The framework inlines those variables into the JavaScript at build time, which is exactly what you want for a public key and exactly what you do not want for a secret one. The other usual path is pasting the key into the Lovable, Bolt or Cursor chat to "connect" a service: the model writes it into the component that needs it. More detail in secrets your app leaks in the browser.
How to fix it
- Rotate the key first. Treat it as compromised even if you remove it from the code: earlier bundles are still in caches and in the deployment history. Generate a new one in the service's dashboard and revoke the old one.
- Move the call to the server. In Next.js, to a Route Handler or a Server Action; in Nuxt, to a
server/apiroute; with Supabase, to an Edge Function; on Netlify, to a function. The browser calls your server and the server, with the key, calls the service. - Drop the public prefix from the variable and store it in your hosting dashboard as a server variable. If a variable needs the prefix, it is public: ask yourself whether it should be.
- Check the git history. If the key was ever pushed to a repository, it is in the history even if the current file is clean.
Check it is clean
curl -s https://yourdomain.com | grep -oE 'src="[^"]+\.js[^"]*"' | cut -d'"' -f2 | head -5
curl -s https://yourdomain.com/assets/index-XXXX.js | grep -oE "sk_live_[A-Za-z0-9]+|rk_live_[A-Za-z0-9]+|AKIA[A-Z0-9]{16}|AIza[A-Za-z0-9_-]{35}|ghp_[A-Za-z0-9]{36}|BEGIN (RSA |EC )?PRIVATE KEY" | head
The first command lists your scripts; the second searches for the patterns in one of them. If nothing comes out of any of them, the bundle is clean. Repeat after every major deployment, because an AI regeneration can put the key back.
Frequently asked questions
Are the Supabase anon key or Stripe's pk_live a problem?
No. They are public by design and the scanner does not flag them. What protects your data with the anon key is RLS; what protects Stripe with the pk_live is that it only serves to create payment tokens, not to charge.
It flags a Google key I use for Maps. Is that serious?
AIza keys can legitimately be public if they are restricted by domain and by API in the Google Cloud console. Without restrictions, anyone can spend your quota. Restrict it and the real risk goes away, even though the pattern still shows up.
I already removed it from the code and it still shows up.
The deployed bundle may be the old one, or the key may be in another file (a shared chunk, a third-party script hosted on your domain). Deploy again, rescan and, in any case, rotate the key.