Back to Blog
critical SEVERITY8 min read

How Unauthenticated API Endpoint Exposure happens in Node.js and how to fix it

A critical vulnerability in `api/firebase-config.js` exposed all Firebase configuration values — including API keys, app IDs, and project IDs — to any unauthenticated caller. With no access controls, CORS restrictions, or rate limiting in place, attackers could retrieve live credentials and directly access Firebase services. The fix adds shared-secret authentication using timing-safe comparison, origin validation, and method enforcement.

O
By Orbis AppSec
Published August 26, 2026Reviewed August 26, 2026

Answer Summary

This vulnerability is an unauthenticated sensitive data exposure (CWE-200) in a Node.js API endpoint (`api/firebase-config.js`). The `/api/firebase-config` handler returned all Firebase credentials — `apiKey`, `appId`, `projectId`, and more — to any HTTP GET request with no authentication check. The fix adds a shared-secret header (`x-firebase-config-secret`) validated with Node.js's `timingSafeEqual`, plus origin allowlisting and HTTP method enforcement, so only authorized callers can retrieve the configuration.

Vulnerability at a Glance

cweCWE-200
fixAdded shared-secret header validation using `timingSafeEqual`, HTTP method enforcement, and origin allowlisting before returning any configuration data
riskAny unauthenticated attacker can retrieve live Firebase API keys and use them to access Firebase services
languageJavaScript (Node.js)
root causeThe `handler` function in `api/firebase-config.js` returned environment-sourced credentials without any authentication, authorization, or origin checks
vulnerabilityUnauthenticated Sensitive Data Exposure via API Endpoint

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:

  1. No authentication: Any caller receives the full config object. There is no check for a token, session cookie, or any credential.
  2. No origin restriction: Cross-origin requests from arbitrary domains are accepted.
  3. 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 signUp via 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 apiKey and projectId can read or write data
  • Storage bucket access: storageBucket combined with apiKey can 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-config endpoint returned live apiKey, projectId, and appId values to any unauthenticated GET request — a two-step exploit away from Firebase account creation or data access.
  • Shared-secret authentication using timingSafeEqual is the right pattern for server-to-server or build-time credential endpoints where JWT-based auth is overkill.
  • Failing closed when FIREBASE_CONFIG_SECRET is 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_ORIGIN adds a second layer that prevents cross-origin browser-based abuse even if the secret were somehow obtained.

How Orbis AppSec Detected This

  • Source: The handler function in api/firebase-config.js reads 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 the x-firebase-config-secret request 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

Frequently Asked Questions

What is unauthenticated API endpoint exposure?

It occurs when a server-side route returns sensitive data — such as API keys or credentials — without verifying the identity or authorization of the caller, allowing anyone with network access to retrieve those secrets.

How do you prevent sensitive credential exposure in Node.js APIs?

Always authenticate callers before returning credentials. Use shared secrets validated with `crypto.timingSafeEqual`, require specific HTTP methods, enforce origin allowlisting, and never return credentials to unauthenticated requests.

What CWE is unauthenticated sensitive data exposure?

CWE-200 (Exposure of Sensitive Information to an Unauthorized Actor) covers cases where an application returns sensitive data to callers who have not been authenticated or authorized.

Is HTTPS alone enough to prevent this type of exposure?

No. HTTPS encrypts data in transit but does nothing to prevent an unauthenticated endpoint from returning secrets to any caller who simply knows the URL. Authentication and authorization controls are required at the application layer.

Can static analysis detect unauthenticated endpoint exposure?

Yes. Static analysis tools — including AI-assisted scanners like Orbis AppSec — can trace data flows from environment variables through API handlers and flag paths where sensitive values are returned without authentication guards.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #1039

Related Articles

critical

How Missing Authentication Middleware Happens in Node.js APIs and How to Fix It

A critical vulnerability in a Node.js Panel Connector API (CVE-2025-7783) left 14 endpoints—including shell command execution, file deletion, and file writing—completely open to unauthenticated access. The comment in the source code even declared "NO AUTH — Full Open Access," making it a textbook example of a missing authentication control. The fix adds a Bearer token middleware guard on all `/api` routes, blocking unauthorized requests before they reach any sensitive handler.

critical

How OAuth CSRF Attacks Happen in Node.js and How to Fix Them

A missing OAuth state parameter validation in `src/account_manager.js` left the `startOAuthServer()` function vulnerable to CSRF attacks, allowing an attacker to inject their own authorization code into a victim's active OAuth session. The fix generates a cryptographically random state token using `crypto.randomBytes()`, returns it alongside the server handle, and rejects any callback where the returned state doesn't match — closing the attack window entirely. This affects all downstream consume

high

How Middleware and Proxy Bypass happens in Next.js App Router and how to fix it

CVE-2026-64642 is a high-severity authentication bypass vulnerability in Next.js that affects App Router applications using Turbopack with a single locale configuration. The flaw allows attackers to circumvent middleware and proxy security controls, potentially gaining unauthorized access to protected routes. Upgrading from Next.js 16.2.7 to 16.2.11 closes the vulnerability entirely.

critical

How OAuth 2.0 CSRF happens in PHP and how to fix it

A critical OAuth 2.0 CSRF vulnerability in `login_weibo.php` allowed attackers to forge Weibo login requests by exploiting the missing `state` parameter validation. Without this check, an attacker could trick a victim's browser into completing an OAuth flow with the attacker's authorization code, potentially hijacking the victim's session. The fix generates a cryptographically random state token, stores it in the session, and validates it on callback.

critical

How Unauthenticated API Endpoints happen in Node.js Express and how to fix it

The `/token` endpoint in `plugin/multiplex/index.js` generated presentation control tokens without verifying the requester's identity, allowing any attacker with network access to seize control of a live reveal.js presentation. The fix restricts token generation to localhost-only requests and replaces a broken cryptographic primitive with a proper SHA-256 hash. Together, these changes eliminate both the access-control gap and a secondary cryptographic weakness in a single targeted patch.

critical

How eval() Code Injection happens in JavaScript and how to fix it

A critical code injection vulnerability was discovered in `js/lib/jsencrypt.js` at line 195, where a direct `eval()` call executed a JavaScript string shim for the `process` object in browser environments. If an attacker could influence the string passed to `eval()`—through a compromised dependency, a man-in-the-middle attack, or supply chain tampering—they could achieve arbitrary JavaScript execution in any user's browser. The fix replaces the `eval()` call with the equivalent inline JavaScript