squirrelscan
GitHub

Leaked API key in client-side code: how to find one

An API key shipped in HTML or a bundle can be read by anyone who views source. squirrel scans every page and script against 87 key patterns.

What a leaked secret is

A leaked secret is a credential that reaches the browser: an API key in a bundle, a database connection string in server-rendered HTML, a token in an inline script. Anything the browser can load, anyone can read, and automated scrapers do read it. A leaked key is used within hours of being published.

Not every key in client-side code is a leak. Stripe publishable keys, Firebase and Google browser keys, OAuth client IDs, Supabase anon keys and Sentry DSNs are designed to ship publicly and are safe as long as their server-side restrictions are configured. The dangerous ones are secret keys, service-role keys, personal access tokens and anything with a database password in it.

What squirrel checks

The rule scans the HTML of every crawled page, every inline <script>, and the content of external JavaScript files the crawl fetched. It matches against 87 patterns covering AI providers, cloud platforms, payment processors, databases, source hosts, messaging services and generic assignments such as api_key = "...". Findings are deduplicated by value across the whole site, and each reported value is masked. It emits these checks:

  • leaked-secrets-high fails when a high-confidence pattern matched. Message: 2 high-confidence leaked secret(s) detected. Each item reads as the pattern name plus the masked value, labelled Found in inline-script (https://example.com/page). Severity error.
  • leaked-secrets-medium warns on medium-confidence matches, which are mostly the generic assignment patterns. Message: 4 potential secret(s) detected (verify manually).
  • leaked-secrets-public reports info for keys that are public by design. Message: 3 public client-side key(s) found (public by design — verify usage restrictions are configured). These never count as leaks.
  • leaked-secrets passes when no high- or medium-confidence match remains. Message: No leaked API keys or secrets detected.

The location on each item is html, inline-script or external-script. Masking keeps the first six and last four characters when the value is longer than twelve, and the first four otherwise, so the report identifies the key without republishing it. Weight is 10, the highest of any rule in the Security category.

How to fix it

// server route, not the client bundle
const res = await fetch("https://api.example.com/v1/data", {
  headers: { Authorization: `Bearer ${process.env.EXAMPLE_SECRET_KEY}` },
});

Move the call behind an endpoint you control and read the key from an environment variable that the bundler never inlines. In a framework with a public-variable prefix, check that the leaked name does not carry it. Rotate the exposed credential before anything else: it has been public for as long as the page has.

A medium-confidence hit on a variable named apiKey holding a placeholder is a false positive. Confirm the value before rotating.

Rule IDsecurity/leaked-secrets
CategorySecurity
ScopeSite-wide
Severityerror
Weight10/10

Enable / disable

Disable this rule

[rules]
disable = ["security/leaked-secrets"]

Disable all Security rules

[rules]
disable = ["security/*"]

Enable only this rule

[rules]
enable = ["security/leaked-secrets"]
disable = ["*"]

Security findings ship in every audit next to the SEO, performance and agent experience rules. See Website security scan with AI for how an agent works through a report.

References

Check your site

Run squirrel audit https://example.com and open the Security section of the report. Every match is listed with its pattern name, masked value and the page or script it came from. Local audits are free.

Type to search…

↑↓ navigate openesc close