What anyone can read in your code
Access keys for databases, AI services and payments, error messages and other traces in your app’s HTML and JavaScript files.
Any visitor can open the source of your homepage and your app’s JavaScript files. In the browser it takes two clicks. If an access key is in there, anyone can copy and use it. We read the homepage HTML and the JavaScript files it includes from your domain. On top of that come up to 40 parts that these files load when someone switches to a subpage, for example the admin area, for Next.js the code of other pages, and configuration files such as /config.json that the code fetches on startup. The browser loads them as well. Keys Base64-encoded with atob() in the code are read decoded. What these findings cost in points is listed in What gets deducted.
Keys in the source and in JavaScript files
An access key in the code is like a house key under the doormat. Whoever finds it gets into your database, your payment account or your AI services, with your permissions and at your expense. If personal data sits behind it, this can become a reportable data breach.
In apps built with Lovable, Bolt, v0, Cursor or Claude Code, keys rarely sit in the HTML. At build time they end up in the JavaScript files, for example under /assets/index-….js with Vite or /_next/static/ with Next.js. That is why we load these files like a browser and search them for these patterns:
| What we look for | What someone can do with it |
|---|---|
| Supabase master key (service_role) and secret Supabase key (sb_secret) | Read, change and delete the entire database, bypassing all access rules |
| Keys for OpenAI, Anthropic, OpenRouter, Groq, xAI, Hugging Face, Replicate, Perplexity or ElevenLabs | AI requests billed to you |
| Secret Stripe keys (sk_live, rk_live) | Depending on permissions, refund payments and fetch customer data |
| AWS access keys | Depending on permissions, storage, servers and billing of your AWS account |
| GitHub and Slack tokens | Depending on permissions, read and change source code or messages |
| SendGrid, Resend, Brevo, Mailchimp and Twilio keys | Email and text messages in your name and at your expense, including phishing with your sender address |
| Access tokens with a distinctive prefix: Supabase account (sbp_), Shopify Admin, Google OAuth (GOCSPX-), webhook signatures (whsec_), Notion, Airtable, Telegram bot, GitLab, npm, DigitalOcean, Netlify, Linear, Mapbox (sk.), PostHog (phx_), Sentry, Pinecone, LangSmith, Cloudinary | Depending on the service, read and change data, trigger orders or messages, inject forged webhook events |
| Webhook URLs for Slack, Discord, Teams or Zapier | Post messages to your channels, including fake instructions |
| Database URLs with passwords, and private keys | Log in to the database directly or impersonate your service |
We look for the same patterns in the homepage HTML, including inline scripts and embedded data such as __NEXT_DATA__ or window.__ENV. Each key type found costs 40 points, whether in the HTML or in a JavaScript file. In the HTML we additionally look for values under names such as client_secret, api_secret or access_token. There, each type costs 25 points, because the name alone is no proof. We name the type and where it was found, meaning the file or the page source, and for values under names such as api_key also the name. We show the value only shortened, for example sk_live_…9f2A or AKIA…MPLE, so you can match it at the provider. For database URLs you see the user and host, with the password blacked out. We never show, store or use the full value.
Three more findings belong here:
- Secret-named variables. Anything with VITE_, NEXT_PUBLIC_, REACT_APP_ or EXPO_PUBLIC_ in its name is built by the tool into the code every browser receives. If SECRET, PRIVATE or PASSWORD follows, there is probably a value there that should never be public.
- Supabase directly from the browser. We recognize the public Supabase key (anon) by the role stored in it. It may sit in the browser, but it only protects the data with access rules on every table. We report this as a notice. How to check the rules is described in Access rules in Supabase.
- AI call from the browser. See AI keys below.
Here is what to do when a key is found:
- Revoke the key and create a new one. Revoke the old one with the provider and create a new one. Deleting it from the code is not enough, because it may long since be in copies, caches and the Git history.
- Keep the new key on the server. It belongs in a server function and in your host’s protected settings, never in code the browser receives. In Lovable or Bolt you can ask the assistant to move the key into a server function.
- Investigate. Check the provider’s logs and billing to see whether anyone used the old key.
- Clarify reporting duties. If personal data was reachable, check with your data protection officer whether you must report it under GDPR Art. 33 (72 hours). The details are in A key is exposed.
We report Google keys in the HTML or JavaScript files only as a notice, because they are often in the browser on purpose, for example for Google Maps. Without restrictions, though, anyone can use such a key for other Google services at your expense. Restrict it in the Google Cloud Console to your domain and the services you need. If it sits in a Firebase configuration, we report Firebase reachable from the browser instead.
AI keys and AI calls from the browser
Many generated apps call OpenAI or Anthropic directly from the browser. For that, the key has to sit in the frontend, where any visitor can read it. Whoever has it can send requests billed to you until the quota or budget runs out. GitGuardian counted 1,275,105 leaked keys for AI services in 2025, 81% more than the year before (State of Secrets Sprawl 2026).
We recognize these calls by settings the OpenAI and Anthropic libraries require to explicitly allow the browser (dangerouslyAllowBrowser), and by the keys themselves when they appear in the JavaScript files.
How to fix it: revoke the key and create a new one. Route every AI call through your own server function, for example a Supabase Edge Function or an API route on Vercel. The browser then talks only to your server. Set a spending limit with the provider.
Unencrypted parts on an HTTPS page
If an encrypted page loads images, scripts or stylesheets over http, that part is unencrypted. Someone on the same network can read or replace it, and in the case of a script, inject their own code into your page. This is called mixed content. We count http:// references in src as well as stylesheets, icons and manifests included via link. Regular links to other pages don’t count, because they don’t load anything.
How to fix it: switch the addresses to https://. Adding upgrade-insecure-requests to the Content-Security-Policy also helps.
Visible error messages
If the page shows text such as “Fatal error”, “Stack trace” or a PHP warning, your server is displaying technical errors publicly. They often contain file paths, file names and versions that make it easier for attackers to look for vulnerabilities.
How to fix it: write errors only to a log file, not to the page. In WordPress you do this in wp-config.php:
define('WP_DEBUG_DISPLAY', false);
@ini_set('display_errors', 0);Third-party scripts without a fingerprint
Scripts from other servers run with full rights on your page. If such a server is tampered with, the modified code runs on your site too and can, for example, capture form input. A fingerprint of the expected file in the integrity attribute prevents this: if the file differs, the browser won’t load it. The method is called Subresource Integrity, or SRI.
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"
integrity="sha384-…" crossorigin="anonymous"></script>Most CDNs show the integrity value next to the file. SRI only works for files with a fixed version. That is why we only count scripts whose address carries a full version number, such as jquery-3.7.1.min.js, @1.2.3 or /1.2.3/. Scripts from services that change all the time, such as tag managers, consent banners, chat, payment or login widgets, don’t count. Commented-out includes such as <!--[if IE]> are not loaded by any current browser and don’t count either.
Source maps
A source map makes minified code readable again. That helps during development. On the live site, though, it lets anyone read your original source code, including comments and internal paths, and search it for weaknesses or forgotten keys. We report when the HTML or one of the JavaScript files references such a file and it is actually served. To check this we request at most three linked .map files and read only their beginning. We don’t store their content.
How to fix it: turn off source maps for the live site in your build tool, for example with build.sourcemap: false in Vite. Most tools already do this by default.
References to sensitive files
If the HTML points in a link, a source or a form to files such as .env, .git or wp-config.php, or to extensions such as .bak, .old, .sql or .swp, we report it as a notice. Files like these often contain passwords and keys. We don’t fetch them, so we don’t know whether they are reachable.
To check it yourself, open the address in your browser. If you see content instead of an error page, remove or block the file right away and change every credential in it.
Still have questions? Email us.