I sympathize. We were ucked by the same issue last night
you can use this script to scan your projects
Prerequisites (before running the script):
- Node.js 18+ (uses native
fetch and top-level await)
- Firebase CLI installed and authenticated:
firebase login
- Google Cloud CLI installed and authenticated:
gcloud auth login
- You must have Owner or Editor access to the GCP projects you want to scan
Usage:
# Scan deployed JS bundles in your local public/ folder only
node check-gemini-key-exposure.mjs
# Scan all GCP projects linked to your gcloud account
node check-gemini-key-exposure.mjs --all-projects
#!/usr/bin/env node
/**
* check-gemini-key-exposure.mjs
*
* Two-mode check:
* 1. (default) Scans all deployed JS bundles in public/ for Firebase API keys
* and tests each against Gemini (TruffleHog PoC).
* 2. (--all-projects) Uses `gcloud` to enumerate every GCP project, lists all
* API keys per project via `gcloud alpha services api-keys list`, and tests
* each key against Gemini.
*
* Usage:
* node scripts/check-gemini-key-exposure.mjs # public/ scan only
* node scripts/check-gemini-key-exposure.mjs --all-projects # all gcloud projects
*
* Exit code:
* 0 - all keys are blocked (safe)
* 1 - at least one key is exposed (action required)
*/
import { readdirSync, readFileSync, appendFileSync, writeFileSync } from "node:fs";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { execSync } from "node:child_process";
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = resolve(__dirname, "..");
const PUBLIC_DIR = resolve(ROOT, "public");
const ALL_PROJECTS = process.argv.includes("--all-projects");
const RESULTS_CSV = resolve(__dirname, "gemini-exposure-report.csv");
// Initialize CSV with header (overwrite any previous run)
writeFileSync(RESULTS_CSV, "project,key,models_status,files_status,status\n");
function saveResult(project, key, modelsStatus, filesStatus, exposed) {
const row = `${project || "public/"},${key},${modelsStatus},${filesStatus},${exposed ? "EXPOSED" : "BLOCKED"}\n`;
appendFileSync(RESULTS_CSV, row);
}
// -- gcloud helpers ------------------------------------------------------------
function gcloud(args) {
try {
return execSync(`gcloud ${args} 2>/dev/null`, { encoding: "utf-8" }).trim();
} catch {
return "";
}
}
function listProjects() {
const out = gcloud('projects list --format="value(projectId)"');
return out ? out.split("\n").filter(Boolean) : [];
}
async function getFirebaseApiKeys(projectId) {
const keys = new Set();
// Approach 1: firebase apps:sdkconfig WEB (for projects with registered WEB apps)
try {
const raw = execSync(
`firebase apps:list --project "${projectId}" --json 2>/dev/null`,
{ encoding: "utf-8" }
);
const parsed = JSON.parse(raw.slice(raw.indexOf("{")));
const webAppIds = (parsed.result || [])
.filter((a) => a.platform === "WEB")
.map((a) => a.appId);
for (const appId of webAppIds) {
try {
const configRaw = execSync(
`firebase apps:sdkconfig WEB --project "${projectId}" "${appId}" --json 2>/dev/null`,
{ encoding: "utf-8" }
);
const configParsed = JSON.parse(configRaw.slice(configRaw.indexOf("{")));
const apiKey = configParsed.result?.sdkConfig?.apiKey || configParsed.result?.apiKey;
if (apiKey) keys.add(apiKey);
} catch {}
}
} catch {}
// Approach 2: /__/firebase/init.json (works even without registered apps)
if (keys.size === 0) {
try {
const res = await fetch(
`https://${projectId}.firebaseapp.com/__/firebase/init.json`,
{ signal: AbortSignal.timeout(6000) }
);
if (res.ok) {
const json = await res.json();
if (json.apiKey) keys.add(json.apiKey);
}
} catch {}
}
return [...keys];
}
// -- public/ scan helpers ------------------------------------------------------
function* walkJs(dir) {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const full = resolve(dir, entry.name);
if (entry.isDirectory()) yield* walkJs(full);
else if (entry.isFile() && entry.name.endsWith(".js")) yield full;
}
}
function extractKeys(filePath) {
const content = readFileSync(filePath, "utf-8");
return [...new Set(content.match(/AIzaSy[A-Za-z0-9_-]{33}/g) || [])];
}
// -- Test key against Gemini endpoints (TruffleHog PoC) -----------------------
async function testGeminiAccess(key) {
const endpoints = [
`https://generativelanguage.googleapis.com/v1beta/models?key=${key}`,
`https://generativelanguage.googleapis.com/v1beta/files?key=${key}`,
];
const results = await Promise.all(
endpoints.map(async (url) => {
const res = await fetch(url, { method: "GET" });
return { url, status: res.status };
})
);
return results;
}
// -- Build key map -------------------------------------------------------------
// Map: keyString -> { sources: string[], project?: string }
const keyMap = new Map();
function addKey(keyString, source, project) {
if (!keyMap.has(keyString)) keyMap.set(keyString, { sources: [], project });
keyMap.get(keyString).sources.push(source);
}
if (ALL_PROJECTS) {
console.log("Enumerating all GCP projects via gcloud...");
const projects = listProjects();
console.log(`Found ${projects.length} project(s).\n`);
for (const projectId of projects) {
process.stdout.write(` ${projectId}... `);
const keys = await getFirebaseApiKeys(projectId);
if (keys.length > 0) {
console.log(`${keys.length} key(s) found`);
for (const key of keys) addKey(key, `firebase/${projectId}`, projectId);
} else {
console.log(`no keys`);
}
}
console.log();
} else {
console.log("Scanning public/ for Firebase API keys...");
for (const filePath of walkJs(PUBLIC_DIR)) {
for (const key of extractKeys(filePath)) {
addKey(key, filePath.replace(ROOT + "/", ""), undefined);
}
}
}
if (keyMap.size === 0) {
console.log("No Firebase API keys found.");
process.exit(0);
}
console.log(`Found ${keyMap.size} unique key(s). Testing against Gemini API...\n`);
let anyExposed = false;
for (const [key, { sources, project }] of keyMap) {
const results = await testGeminiAccess(key);
const exposed = results.some((r) => r.status === 200);
const label = exposed ? "EXPOSED" : "BLOCKED";
const projectTag = project ? ` [${project}]` : "";
const [modelsResult, filesResult] = results;
saveResult(project, key, modelsResult.status, filesResult.status, exposed);
console.log(`${label} ${key}${projectTag}`);
console.log(
` Source: ${sources.slice(0, 3).join(", ")}${sources.length > 3 ? ` (+${sources.length - 3} more)` : ""}`
);
for (const { url, status: httpStatus } of results) {
const endpoint = new URL(url).pathname;
console.log(` ${endpoint} -> HTTP ${httpStatus}`);
}
console.log();
if (exposed) anyExposed = true;
}
if (anyExposed) {
console.error("ACTION REQUIRED: At least one key is exposed to Gemini.");
console.error("Steps:");
console.error(" 1. Go to Google Cloud Console -> APIs & Services -> Credentials");
console.error(
" 2. Find the exposed key and add apiTargets restriction (exclude generativelanguage.googleapis.com)"
);
console.error(" 3. Also add HTTP referrer restrictions to limit key usage to your domains");
console.error(" 4. Re-run this script to confirm the fix");
process.exit(1);
} else {
console.log("All keys are blocked from Gemini. Project is safe.");
}
console.log(`\nResults saved to: ${RESULTS_CSV}`);