Exposed Secrets: API Keys and Tokens in Your HTML
Everything in a web page, including inline scripts, configuration objects and comments, is sent to every visitor. A secret key in the HTML is not hidden by minification or obscure variable names. Automated scanners crawl the web looking for exactly these patterns, and keys found this way can be abused quickly: cloud servers started for crypto mining, payment refunds, emails sent in your name, or paid AI APIs billed to you.
How keys end up in pages
- A developer calls a third-party API directly from the browser to save building a backend endpoint.
- A build tool inlines environment variables into the front-end bundle. Frameworks usually expose only variables with a public prefix, but a variable can be given that prefix by mistake.
- A configuration object is rendered into the page for “debugging” and never removed.
- A theme or plugin settings page stores a key that the template then prints.
What to do when a key is exposed
1. Revoke or rotate it first. Removing the key from the page does not help: it has been public, and copies exist in caches, archives and attackers’ lists. Generate a new key, deploy it to the server, then revoke the old one in the provider’s console (AWS IAM, Stripe dashboard, GitHub settings, Slack app settings, SendGrid, OpenAI or Anthropic console).
2. Check for misuse. Look at the provider’s usage and billing pages and audit logs for activity you do not recognise since the key appeared.
3. Move the call to the server. The browser talks to your backend, and your backend calls the provider with the key from its environment:
// Server route (Node.js)
app.post('/api/summary', async (req, res) => {
const r = await fetch('https://api.example-ai.com/v1/complete', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.AI_API_KEY}` },
body: JSON.stringify({ prompt: req.body.text }),
});
res.json(await r.json());
});
Add your own authentication and rate limiting to that route, or it becomes an open proxy to the paid API.
4. Look for other copies. The same key may be in your Git history, other pages or mobile apps. Secret-scanning tools in your repository host can find them.
Keys that are meant to be public
Some keys are designed to be used in browsers: Google Maps and Firebase web configuration keys,
Stripe publishable keys (pk_live_...), reCAPTCHA and Turnstile site keys. They identify your
project; they are not secrets. They still need limits, because anyone can copy them:
- Google API keys: in the Google Cloud console, under Credentials, restrict the key to your
websites (HTTP referrers such as
https://example.com/*) and to only the APIs it needs. Unrestricted keys can be used by others against your quota and bill. - Firebase: protect data with Security Rules and App Check; the API key alone grants nothing.
How PerfoMetrix checks this
We search the HTML we already downloaded, including inline scripts, for well-known key formats. We send no extra requests to your site and do not fetch external script files.
- Fail: an AWS access key, a Stripe live secret or restricted key, a private key block, a Slack token, a GitHub token, a SendGrid key, or an OpenAI or Anthropic API key.
- Needs attention: only Google API keys (often public by design; make sure they are restricted).
- Pass: none of these.
The report shows each finding masked (the first four and last two characters), never the full key. We do not test whether a key works. A key that matches the pattern but is already revoked still shows up here; remove it from the page anyway.
Pattern matching cannot find every secret: custom tokens, passwords and keys from other providers have no recognisable format. Treat a pass as “none of the common formats found”, not as proof the page is clean.
Further reading: OWASP: Secrets Management Cheat Sheet and Google Cloud: API key best practices.