Client-Side API Keys Are Credentials, Even in Headers
A critical flaw in AI model discovery code was handing Google Generative AI API keys directly to end users. The discoverAvailableModels() function accepted an API key parameter and embedded it into a fetch URL as a query string:
const listResponse = await fetch(
`https://generativelanguage.googleapis.com/v1/models?key=${apiKey}`
);
In client-side code, this pattern is catastrophic. Every user who opens browser DevTools, inspects network traffic, or reviews the bundled JavaScript can see the key. Search engine crawlers may index it. Logs may record it. The key becomes as exposed as a hardcoded password in a public repository.
For Google Generative AI endpoints, the fix is to use the x-goog-api-key HTTP header instead:
const listResponse = await fetch(
'https://generativelanguage.googleapis.com/v1/models',
{ headers: { 'x-goog-api-key': apiKey } }
);
This is not a cure—it is a necessary harm reduction. Even in a header, a client-side key is still client-side. But headers are not part of the URL, not indexed by search engines, and not visible in browser history. They also allow backend-driven revocation and origin-specific CORS policies, which query parameters do not.
Why Query Parameters Are Worse Than Headers
When an API key lives in the URL:
- Search engine indexing: Crawlers visiting your application or intercepted requests may cache the full URL with the key.
- Referrer headers: A link from your site to an external domain will send the referrer header, leaking the key to that domain.
- Browser history: The full URL is stored in local browser history and sync services.
- Proxy logs: Any proxy, CDN, or corporate firewall logs the full request URI.
- Static bundle analysis: The key is visible in minified JavaScript without even running the code.
HTTP headers are request metadata—not part of the URL. They are not logged by default, not sent in referrers, and not visible in browser history. Search engines ignore headers for indexing.
This does not make client-side keys safe. It makes them marginally less universally exposed.
The Attack Surface
An attacker (or a curious user) who extracts the Google API key can:
- Make unlimited requests to your quota, exhausting it and breaking your application.
- Generate content using your billing account, incurring charges.
- Map the key to your organization and perform reconnaissance on your infrastructure.
- Revoke your own key by capturing it and then replacing it in their own client, locking you out of your own API.
For a production application, this is not theoretical—it is inevitable. API keys in client-side code will be harvested within days.
The Fix in Context
The pull request changed two things:
- Removed the query parameter interpolation:
?key=${apiKey}becomes a plain endpoint URL. - Passed the key via the
headersoption in the fetch configuration:{ headers: { 'x-goog-api-key': apiKey } }.
The Google Generative AI API accepts both forms, but the header form is the intended client-side pattern. It keeps the key out of the URL and makes credential rotation and origin-locking feasible from the backend.
// Before (vulnerable)
const listResponse = await fetch(
`https://generativelanguage.googleapis.com/v1/models?key=${apiKey}`
);
// After (mitigated)
const listResponse = await fetch(
'https://generativelanguage.googleapis.com/v1/models',
{ headers: { 'x-goog-api-key': apiKey } }
);
This change applies to both discoverAvailableModels() and initializeModel() functions, which share the same authentication pattern.
The Larger Problem: Client-Side Keys Should Not Exist
This fix is tactical. The strategic problem is that API keys should not be in client-side code at all.
In a production architecture:
- Frontend calls your own backend (or a same-origin proxy).
- Backend holds the Google API key and makes requests to Google on behalf of the frontend.
- Frontend never sees the key.
This pattern is called a "backend-for-frontend" (BFF) or API proxy. It allows you to:
- Rotate credentials without redeploying client code.
- Apply rate limits, logging, and access control at the backend.
- Monitor your own quota usage and costs.
- Revoke a leaked key without breaking the frontend.
The header-based fix allows you to move toward this architecture incrementally. If you must embed a key in the frontend (for prototyping, open-source projects, or client-side-only applications), use headers and restrict the key's permissions in the Google Cloud Console to the bare minimum—typically read-only access and a specific API method.
How Orbis AppSec Detected This
Source: The apiKey parameter passed to discoverAvailableModels() and initializeModel() functions.
Sink: The fetch() call that constructs the URL with template string interpolation: `https://generativelanguage.googleapis.com/v1/models?key=${apiKey}`.
Missing control: No validation that the key is not embedded in a URL. The functions accepted the key and immediately put it into the most exposed place possible—the query string.
CWE: CWE-798 (Use of Hard-Coded, Security-Relevant Constants).
Fix: Move the API key from URL query parameter to the x-goog-api-key HTTP header.
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
API keys in client-side code are a permanent leak. Headers reduce the attack surface compared to query parameters by keeping credentials out of URLs, logs, and history—but they do not eliminate the exposure. The real fix is to never embed credentials in client-side code at all. Use a backend proxy, restrict the key's permissions in your API provider's console, and treat any client-side key as compromised from the moment it ships.