usability.cat
Issue Wiki

Exposed API Keys

Writing your PIN on the back of your debit card — that's what hardcoding API keys in client-side code does. Anyone can see them.

What is this?

Imagine writing your bank PIN on the back of your debit card. Anyone who sees the card — a waiter, a cashier, someone standing behind you — now has everything they need to drain your account. That is exactly what happens when you put API keys, secret tokens, or credentials in your client-side JavaScript. Every visitor to your site can open browser DevTools, read your source code, and extract those keys. Bots scan the internet for exposed keys automatically and exploit them within minutes.

Why it matters

  • For your visitors: Exposed API keys can be used to access your backend services, potentially exposing user data, sending emails on your behalf, or modifying records. If an attacker gets your database key, your visitors' personal information is compromised.
  • For your business: Financial damage is real and immediate. Exposed cloud provider keys (AWS, GCP, Azure) have led to bills in the tens of thousands of dollars overnight from cryptomining. Exposed payment processor keys can enable fraud. Exposed email API keys turn your domain into a spam cannon, getting it blacklisted. This is not hypothetical — it happens daily.
  • The standard: API keys and secrets must never appear in client-side code. Use environment variables on the server, backend API routes as proxies, and public-only keys (like Stripe's publishable key) on the client. If a key starts with sk_ or secret_, it must never leave the server.
Key stays on the server
// src/app/api/weather/route.ts (server-side only)
export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const city = searchParams.get("city");
  const res = await fetch(
    `https://api.weather.com/data?q=${city}&key=${process.env.WEATHER_API_KEY}`
  );
  return Response.json(await res.json());
}
Key exposed in client code
// This runs in the browser — everyone can see it!
"use client";
const API_KEY = "sk_live_abc123xyz789secret";

async function getWeather(city: string) {
  const res = await fetch(
    `https://api.weather.com/data?q=${city}&key=${API_KEY}`
  );
  return res.json();
}

How to fix it

React / Next.js

Use server-side API routes as a proxy. The secret key lives on the server; the client talks to your API route.

// src/app/api/send-email/route.ts — SERVER ONLY
// This file runs on the server. The API key never reaches the browser.
export async function POST(request: Request) {
  const { to, subject, body } = await request.json();

  const res = await fetch("https://api.sendgrid.com/v3/mail/send", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.SENDGRID_API_KEY}`, // server env var
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ to, subject, body }),
  });

  return Response.json({ success: res.ok });
}
// Client component — calls YOUR API, not the third-party directly
"use client";

async function sendEmail(to: string, subject: string, body: string) {
  const res = await fetch("/api/send-email", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ to, subject, body }),
  });
  return res.json();
}

In Next.js, only environment variables prefixed with NEXT_PUBLIC_ are exposed to the browser. Use this to your advantage:

# .env.local
# This is ONLY available on the server — safe for secrets
DATABASE_URL="postgres://user:pass@host/db"
STRIPE_SECRET_KEY="sk_live_abc123"

# This IS exposed to the browser — only use for public keys
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="pk_live_xyz789"

Plain HTML

Never embed secrets in HTML or client-side JavaScript. Use a backend to proxy requests.

<!-- BAD: API key visible in page source -->
<script>
  // Anyone can see this in View Source or DevTools
  const API_KEY = "secret_abc123";
  fetch(`https://api.example.com/data?key=${API_KEY}`);
</script>

<!-- GOOD: Call your own backend, which holds the key -->
<script>
  // Your backend at /api/data holds the secret key
  fetch("/api/data")
    .then((res) => res.json())
    .then((data) => renderData(data));
</script>

If you discover you have already exposed a key, rotate it immediately — assume it has been compromised.

High impactsecurity~2 paws

Exposed API keys are the easiest vulnerability to exploit and the most common one the cat finds. Rotate compromised keys immediately and keep secrets on the server.

How the cat scores this

The scanner analyzes client-side JavaScript for patterns that look like API keys, secrets, and tokens. It checks for common key prefixes (sk_live_, sk_test_, AKIA, ghp_, api_key=, secret=), high-entropy strings assigned to suspiciously named variables, and known API endpoint patterns with inline credentials. The scanner does not report the actual key values — just their presence and location. Each exposed key is flagged as a high-severity finding.

Further reading

On this page