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_orsecret_, it must never leave 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());
}// 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.
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
- OWASP: Sensitive Data Exposure — guide to finding leaked secrets
- GitGuardian: State of Secrets Sprawl — annual report on exposed secrets
- Next.js: Environment Variables — how NEXTPUBLIC prefix works
Exposed Source Maps
Publishing the blueprint of your house online — that's what leaving source maps in production does. Attackers can read your entire codebase.
Unrestricted File Upload
A mailbox that accepts packages of any size with no screening — that's file upload without restrictions. Time to add some rules.