Originally published on Extenify.
If you want to hide an API key in a Chrome extension, here is the short answer: you can't. Every file in your package lands on the user's disk, and 2026 research found hundreds of AI platform keys sitting in real extensions. Below: the three patterns that do work, and the rules that stop a backend proxy from becoming a free AI API for strangers.
Why every trick fails
An installed extension is just the signed ZIP you uploaded, unpacked into the browser profile. Anyone can download the package for a public extension, unzip it and grep the JavaScript.
The usual workarounds all break for the same reason, the key still has to exist on the client:
-
.envat build time.process.env.OPENAI_API_KEYis replaced by a string literal in the bundle. The file stays home; the value ships. - Base64, char codes, split strings. They cost an attacker minutes, and the Chrome Web Store treats them as obfuscation (its code readability rule names Base64 and character encoding, rejection ID "Red Titanium").
-
WebAssembly or a native helper. The key ends up in the outgoing request, and the extension's DevTools Network panel shows the
Authorizationheader in plain text. -
chrome.storage. It protects nothing from the person using the browser.
Google's own Gemini docs say it plainly: keys in client-side code can be extracted, so run a backend proxy.
This is already happening
The CRX-ray study (ACM ASIA CCS, June 2026) scanned 163,924 extensions across Chrome, Firefox and Edge. It found 3,677 unique leaked API keys in 4,145 extensions, including 300 AI platform keys in 309 extensions serving over a million users combined.
A leaked AI key means strangers' traffic on your bill, then an outage while you rotate the key and wait for an update to clear store review.
Three patterns that work
| Pattern | Who pays | Good fit |
|---|---|---|
| Backend proxy | You | Most AI features, free tiers, paid plans |
| Bring your own key | The user | Developer tools, power users |
| Short-lived tokens | You | Realtime or live APIs that support them |
Short-lived tokens only exist for specific APIs: OpenAI's Realtime API can mint ephemeral keys for a browser WebRTC session, and Google's ephemeral tokens (preview) work only with the Gemini Live API. They can still be extracted; they're safer only because they expire fast.
Bring your own key is fine for dev tools. Keep the key in chrome.storage.local, never storage.sync (which copies it to every signed-in browser), send it only to the provider and never to your server, and say so in your privacy policy.
For ordinary text generation, use a proxy.
The proxy rules that matter
The extension talks only to your API. Your API knows the user, decides whether they may make the call, and adds the real key server-side. The important part is what the server refuses to let the client decide:
- Authenticate. The user signs in and gets a revocable session token. Store it hashed so a database leak doesn't hand out live sessions.
-
Expose tasks, not a pipe.
POST /v1/summarize, notPOST /v1/anything. The model, system prompt, input cap andmax_tokensare chosen on the server. A relay that forwards the client's model and prompt is an open AI API with your name on the invoice. -
Count before you call. Increment the user's daily counter before hitting the provider, so a client retrying in a loop hits your limit instead of your budget. Return
429when they're over. - Meter tokens per user per day. The same rows become your cost report per plan.
- Return generic errors. Upstream error bodies can leak account details.
Here's the core of a handler as a sketch (helpers like getUserId, bumpCounter and callModel are yours to write against your DB and provider):
// POST /v1/summarize
export async function summarizeHandler(req, res) {
const userId = await getUserId(req.headers.authorization);
if (!userId) return res.status(401).json({ error: 'sign_in_required' });
const text = String(req.body?.text ?? '').slice(0, 8000);
if (!text.trim()) return res.status(400).json({ error: 'text_required' });
const day = new Date().toISOString().slice(0, 10);
const count = await bumpCounter(userId, day); // atomic upsert, unique on (userId, day)
if (count > 50) return res.status(429).json({ error: 'daily_limit' });
try {
const { summary, usage } = await callModel({ text, maxTokens: 400 }); // model fixed server-side
await addTokens(userId, day, usage);
res.json({ summary });
} catch {
res.status(502).json({ error: 'failed' });
}
}
On the extension side, call it from the service worker, not a content script, since content scripts live inside web pages and anything you hand them can leak to the page. Add your API origin to host_permissions, check sender.id === chrome.runtime.id in your message listener, and keep the session token in chrome.storage.local. A session token on the client is fine: it is per-user, expires, is revocable and limited by your quota.
Add HTTPS, a per-IP edge limit and a provider spend alert.
Gemini users: check your keys
Per Google's docs, all new AI Studio keys created since May 28, 2026 are authorization keys bound to a service account, and the Gemini API rejects unrestricted standard keys. Key restrictions are no substitute for a proxy either: they cover websites, IPs and mobile apps, not extension IDs.
If a key leaks anyway: deploy a replacement on the server, disable the old one, audit billing for unknown traffic, and if it lived in the package, ship an update and assume every installed copy still holds it. With a proxy, the first two steps take minutes and skip store review.
Key takeaways
- Anything inside your extension package, including build-time env values, is public.
- Obfuscation fails technically and gets you rejected from the Chrome Web Store.
- Use a backend proxy for your own keys, BYOK for developer tools, and ephemeral tokens only where providers offer them.
- A safe proxy authenticates users, exposes fixed tasks, enforces quotas before the upstream call and meters tokens.
- Unrestricted Gemini standard keys are now rejected, so audit old builds.
The full guide on hiding an API key in a Chrome extension with a Node.js backend proxy has the complete ~120-line server, manifest and service worker code. Once your AI feature ships, Extenify's browser extension review and rating monitoring helps you see how users react to it across Chrome, Edge and Firefox.
Have you ever found a live API key inside an extension (yours or someone else's), and how did you end up handling it?
Top comments (0)