← All checks
SecurityTypical severity: Critical

JavaScript Secret Scanner

Scans your browser JavaScript for leaked API keys and credentials like Stripe, AWS or Supabase.

What it checks

This scanner reviews the JavaScript your site sends to the browser, looking for patterns of API keys and credentials such as Stripe, AWS or Supabase. It flags secrets that were mistakenly bundled into the client-side code. It only inspects what is already public to any visitor.

Why it matters

Anything shipped in browser JavaScript is visible to anyone who opens the developer tools. A secret key there can be used to access your services, spend your money or read your users' data. It is a very common mistake when server and client variables get mixed up.

How to fix it

Move all secret keys to the server and access them only from the backend, never from browser code. Use only public or restricted keys on the client, and rotate any secret that has already been exposed. In many frameworks, variables without the public prefix stay on the server only.

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/api route; 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.

Check it on your site

Scan your site free and see if this and 44 other checks pass, with a fix prompt for every issue.

Scan my site free

From the blog

Related checks