The Problem: A Door Left Wide Open
The api/firebase-config.js file has one job: deliver Firebase configuration to the frontend so it can initialize the Firebase SDK. That sounds reasonable — until you realize the endpoint was doing this for everyone, with no questions asked.
Before the fix, any HTTP client in the world could send a single GET request:
GET /api/firebase-config
And receive a full JSON payload containing live Firebase credentials:
{
"apiKey": "AIzaSy...",
"authDomain": "myapp.firebaseapp.com",
"projectId": "myapp-prod",
"storageBucket": "myapp.appspot.com",
"messagingSenderId": "123456789",
"appId": "1:123456789:web:abc123",
"measurementId": "G-XXXXXXXX"
}
No token. No session. No secret. Just ask and receive.
This is a critical unauthenticated sensitive data exposure vulnerability — and it's the kind that's easy to miss precisely because the intent seems harmless. Of course the frontend needs these values. The mistake is in how they're delivered.
The Vulnerability Explained
What the Original Code Did
The original handler function in api/firebase-config.js assembled Firebase config from environment variables and returned it unconditionally:
// BEFORE — no authentication, no origin check, no method restriction
export default async function handler(req, res) {
const config = {
apiKey: process.env.FIREBASE_API_KEY || process.env.VITE_FIREBASE_API_KEY || "",
authDomain: process.env.FIREBASE_AUTH_DOMAIN || process.env.VITE_FIREBASE_AUTH_DOMAIN || "",
// ... more fields
};
return res.status(200).json(config);
}
There are three compounding problems here:
- No authentication: Any caller receives the full config object. There is no check for a token, session cookie, or any credential.
- No origin restriction: Cross-origin requests from arbitrary domains are accepted.
- No method restriction: The handler responds to any HTTP method, not just GET.
Why Firebase Credentials Are Dangerous in the Wrong Hands
Firebase API keys are not simply read-only identifiers. Depending on Firebase Security Rules configuration, a leaked apiKey combined with a projectId can enable:
- Account enumeration: Using the Firebase Auth REST API to test whether email addresses exist in the project (
identitytoolkit.googleapis.com/v1/accounts:createAuthUri) - Unauthorized account creation: Calling
signUpvia the Firebase Auth REST API if new user registration is not restricted - Direct Firestore or Realtime Database access: If security rules are permissive (a common misconfiguration), an attacker with
apiKeyandprojectIdcan read or write data - Storage bucket access:
storageBucketcombined withapiKeycan expose files if storage rules are weak
The exploitation scenario is concrete and low-effort:
# Step 1: Retrieve credentials — no auth required
curl https://yourapp.com/api/firebase-config
# Step 2: Use credentials against Firebase REST API directly
curl "https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=AIzaSy..." \
-H "Content-Type: application/json" \
-d '{"email":"attacker@evil.com","password":"password123","returnSecureToken":true}'
That two-step chain — retrieve config, then abuse Firebase — is exactly what the PR's threat model describes as a "2-step chain complexity" exploit.
The Fix
The fix introduces three layered controls in api/firebase-config.js, each addressing a distinct attack surface.
Change 1: HTTP Method Enforcement
// AFTER
if (req.method !== "GET") {
return res.status(405).json({ error: "Method not allowed" });
}
Simple but important: the endpoint now explicitly rejects anything that isn't a GET request. This prevents method-based abuse (e.g., using POST to bypass naive method-checking middleware elsewhere).
Change 2: Shared-Secret Authentication with Timing-Safe Comparison
This is the core of the fix. A new environment variable, FIREBASE_CONFIG_SECRET, acts as a shared secret between the server and any authorized caller (typically the frontend build process or a server-side component):
// AFTER — import at top of file
import { timingSafeEqual } from "crypto";
// Inside handler:
const configSecret = process.env.FIREBASE_CONFIG_SECRET;
if (!configSecret) {
return res.status(401).json({ error: "Unauthorized" });
}
const provided = req.headers["x-firebase-config-secret"] || "";
const secretBuf = Buffer.from(configSecret);
const providedBuf = Buffer.from(
provided.padEnd(configSecret.length, "\0").slice(0, configSecret.length)
);
if (provided.length !== configSecret.length || !timingSafeEqual(secretBuf, providedBuf)) {
return res.status(401).json({ error: "Unauthorized" });
}
Several security properties are carefully preserved here:
Why timingSafeEqual? A naive string comparison (provided === configSecret) is vulnerable to timing attacks. An attacker making many requests can measure response time differences to infer how many characters of their guess are correct, effectively brute-forcing the secret one character at a time. crypto.timingSafeEqual compares buffers in constant time regardless of where the first mismatch occurs.
Why pad and slice? timingSafeEqual requires both buffers to be the same length. The code normalizes the provided value to match the expected secret's length before comparison — but critically, it also checks provided.length !== configSecret.length before the buffer comparison to reject wrong-length inputs immediately (without leaking length information through the buffer manipulation itself).
Why fail closed when FIREBASE_CONFIG_SECRET is unset? If the environment variable isn't configured, the endpoint returns 401 rather than falling through to return credentials. This prevents a misconfigured deployment from accidentally being open.
Change 3: Origin Allowlisting
// AFTER
const allowedOrigin = process.env.ALLOWED_ORIGIN;
const requestOrigin = req.headers.origin || "";
if (allowedOrigin && requestOrigin !== allowedOrigin) {
return res.status(403).json({ error: "Forbidden" });
}
When ALLOWED_ORIGIN is set, the handler rejects requests from any other origin. This prevents cross-origin abuse from attacker-controlled pages attempting to call the endpoint via a victim's browser session.
Change 4: New .env.example Entry
+FIREBASE_CONFIG_SECRET= # Required: shared secret for the /api/firebase-config endpoint
The .env.example update is not just documentation hygiene — it signals to every developer setting up the project that this variable is required, not optional. Without it, the endpoint refuses to serve credentials at all.
Before vs. After at a Glance
| Property | Before | After |
|---|---|---|
| Authentication | None | Shared secret via x-firebase-config-secret header |
| Secret comparison | N/A | timingSafeEqual (constant-time) |
| Origin restriction | None | ALLOWED_ORIGIN env var enforcement |
| Method restriction | None | GET only (405 for others) |
| Fail-safe behavior | Returns credentials | Returns 401 if secret unconfigured |
Prevention & Best Practices
1. Never Return Credentials Without Authentication
Any endpoint that returns API keys, tokens, or configuration secrets must authenticate the caller first. Even if the data "needs to be public for the frontend," consider whether it truly needs to be served dynamically at all — many Firebase configurations can be embedded at build time rather than fetched at runtime.
2. Use Timing-Safe Comparisons for Secrets
Whenever comparing secrets, tokens, or passwords, use your language's constant-time comparison function:
- Node.js:
crypto.timingSafeEqual(a, b) - Python:
hmac.compare_digest(a, b) - Go:
subtle.ConstantTimeCompare(a, b) - Java:
MessageDigest.isEqual(a, b)
Never use ===, ==, or .equals() for secret comparison.
3. Apply Defense in Depth
The fix demonstrates layered security: method checking, secret authentication, and origin allowlisting. No single control is relied upon exclusively. If one layer is misconfigured, the others still provide protection.
4. Fail Closed, Not Open
When a required security configuration (like FIREBASE_CONFIG_SECRET) is absent, the secure default is to deny access — not to fall through and serve sensitive data. This is the "fail secure" principle from OWASP.
5. Audit All Endpoints That Return Environment Variables
Search your codebase for patterns like process.env.FIREBASE_ or process.env.*_KEY appearing in API handler responses. Each one is a candidate for this class of vulnerability.
Relevant Standards
- OWASP API Security Top 10 — API2:2023: Broken Authentication
- OWASP API Security Top 10 — API3:2023: Broken Object Property Level Authorization
- CWE-200: Exposure of Sensitive Information to an Unauthorized Actor
- CWE-208: Observable Timing Discrepancy (addressed by
timingSafeEqual)
Key Takeaways
- The
/api/firebase-configendpoint returned liveapiKey,projectId, andappIdvalues to any unauthenticated GET request — a two-step exploit away from Firebase account creation or data access. - Shared-secret authentication using
timingSafeEqualis the right pattern for server-to-server or build-time credential endpoints where JWT-based auth is overkill. - Failing closed when
FIREBASE_CONFIG_SECRETis unset prevents a misconfigured deployment from silently exposing credentials — a subtle but critical design choice in the fix. - Firebase API keys are not inert identifiers: combined with permissive security rules, they grant direct access to Auth, Firestore, and Storage APIs.
- Origin allowlisting via
ALLOWED_ORIGINadds a second layer that prevents cross-origin browser-based abuse even if the secret were somehow obtained.
How Orbis AppSec Detected This
- Source: The
handlerfunction inapi/firebase-config.jsreads Firebase credentials from environment variables (process.env.FIREBASE_API_KEY,process.env.FIREBASE_APP_ID, etc.) - Sink:
res.status(200).json(config)— the assembled config object, containing all credential values, is serialized and returned in the HTTP response body - Missing control: No authentication check, no origin validation, and no method restriction existed between the source and the sink; any HTTP client could reach the return statement
- CWE: CWE-200 — Exposure of Sensitive Information to an Unauthorized Actor
- Fix: Added
timingSafeEqual-based shared-secret validation against thex-firebase-config-secretrequest header, with origin allowlisting and method enforcement, before any credentials are returned
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
The api/firebase-config.js vulnerability is a reminder that "the frontend needs this data" is not a justification for skipping authentication. A single unguarded endpoint returning Firebase credentials is all an attacker needs to pivot into account enumeration, unauthorized registrations, or — in the worst case — full data access if security rules are misconfigured.
The fix is elegant precisely because it doesn't over-engineer: a shared secret, validated in constant time, with a hard fail when the secret is absent. Combined with origin allowlisting and method enforcement, the endpoint now has meaningful defense in depth.
If your codebase has any endpoint that reads from process.env and writes to a response body, it deserves the same scrutiny applied here.
References
- CWE-200: Exposure of Sensitive Information to an Unauthorized Actor
- CWE-208: Observable Timing Discrepancy
- OWASP API Security Top 10
- OWASP Authentication Cheat Sheet
- Node.js
crypto.timingSafeEqualDocumentation - Semgrep rules: unauthenticated endpoint
- fix: the /api/firebase-config endpoint returns all f... in...