Back to Blog
high SEVERITY5 min read

How API key exposure and ReDoS happens in Node.js and how to fix it

A critical vulnerability in `roll/openai.js` could expose OpenAI API keys to client-side JavaScript bundles, allowing attackers to extract secrets from browser developer tools. Additionally, a Regular Expression Denial of Service (ReDoS) pattern in the `generateErrorMessage()` method could crash the process. Both issues were fixed with targeted, minimal code changes.

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

Answer Summary

This vulnerability involves API key exposure (CWE-200) and Regular Expression Denial of Service (CWE-1333) in a Node.js OpenAI integration module. The `addApiKey()` method loaded secrets from `process.env` without checking if the code was running in a browser context, risking key leakage in client-side bundles. The `generateErrorMessage()` method used a dynamically constructed RegExp with user input, enabling ReDoS. The fix adds a `typeof window !== 'undefined'` guard to prevent key loading in browsers and replaces the unsafe `RegExp` with `String.prototype.slice()`.

Vulnerability at a Glance

cweCWE-200 (Information Exposure), CWE-1333 (ReDoS)
fixAdded browser environment guard in `addApiKey()` and replaced `new RegExp()` with `String.slice()` in `generateErrorMessage()`
riskUnauthorized API usage via exposed keys; process crash via crafted input
languageJavaScript (Node.js)
root causeNo environment check before loading secrets; unsafe dynamic RegExp construction with user input
vulnerabilityAPI Key Exposure + Regular Expression Denial of Service

How API Key Exposure and ReDoS Happens in Node.js and How to Fix It

Introduction

In the roll/openai.js module—a production OpenAI integration handling chat completions, image generation, and error messaging—we discovered two distinct but equally dangerous vulnerabilities. The first, at line 612 in the addApiKey() method, loads API secrets from process.env without verifying the execution environment, meaning those keys could end up in a client-side JavaScript bundle visible to any user with browser developer tools. The second, at line 907 in generateErrorMessage(), constructs a regular expression dynamically from user-controlled input, creating a Regular Expression Denial of Service (ReDoS) vector that could crash the Node.js process.

These aren't theoretical risks. The module is part of a web service where request handlers are directly reachable by remote attackers, and the brace-expansion dependency (CVE-2026-14257) in yarn.lock compounds the denial-of-service surface by allowing unbounded expansion that triggers out-of-memory crashes.


The Vulnerability Explained

Vulnerability 1: API Key Exposure via Client-Side Bundling

The addApiKey() method iterates over environment variables to populate an internal this.apiKeys array:

addApiKey() {
    this.apiKeys = [];
    let base = 0;
    for (let index = 1; index < 100; index++) {
        // Loads OPENAI_SECRET_0, OPENAI_SECRET_1, etc.
        // ...
    }
}

If this module is ever included in a client-side bundle (via Webpack, Vite, or similar), modern bundlers may resolve process.env.OPENAI_SECRET_0 at build time and inline the actual key value into the output JavaScript. An attacker would simply:

  1. Open browser developer tools on the web application
  2. Search the bundled JavaScript for sk- (OpenAI key prefix)
  3. Extract the key from the this.apiKeys array initialization
  4. Use the stolen key to make unlimited OpenAI API calls at the victim's expense

This is especially dangerous because OpenAI API keys grant access to expensive compute resources and potentially sensitive training data.

Vulnerability 2: ReDoS in generateErrorMessage()

The original code constructed a regular expression from user-controlled input:

const commandType = inputText.match(/^\.(ai|ait|aimage)[mh]?/i)?.[0] || '.ai';
const cleanInput = inputText.replace(new RegExp(`^${commandType}`, 'i'), '');

While commandType is constrained by the first regex match, the use of new RegExp() with string interpolation is a dangerous pattern. If commandType contained regex metacharacters (e.g., through future code changes or edge cases), an attacker could craft inputText that causes catastrophic backtracking, freezing the event loop and crashing the process.

Combined with CVE-2026-14257 in brace-expansion (which allows unbounded memory allocation via crafted expansion patterns), the denial-of-service attack surface was significant.


The Fix

Two surgical changes were applied to roll/openai.js:

Fix 1: Environment Guard in addApiKey() (Line 613)

Before:

addApiKey() {
    this.apiKeys = [];
    let base = 0;
    // ... loads keys from process.env
}

After:

addApiKey() {
    if (typeof window !== 'undefined') return;
    this.apiKeys = [];
    let base = 0;
    // ... loads keys from process.env
}

The typeof window !== 'undefined' check is the standard idiom for detecting a browser environment. If this code somehow ends up in a client-side bundle, it will short-circuit immediately, never populating this.apiKeys with sensitive values. This is a defense-in-depth measure—the module shouldn't be bundled client-side at all, but this guard ensures secrets stay safe even if it is.

Fix 2: Replace Dynamic RegExp with String.slice() (Line 908)

Before:

const cleanInput = inputText.replace(new RegExp(`^${commandType}`, 'i'), '');

After:

const cleanInput = inputText.slice(commandType.length);

This eliminates the regular expression entirely. Since commandType is always a prefix of inputText (it was extracted from inputText via the preceding match), String.prototype.slice() achieves the same result with zero regex overhead and zero ReDoS risk. It's faster, simpler, and immune to metacharacter injection.


Prevention & Best Practices

  1. Never trust bundler boundaries: Even if you "know" a module is server-only, add runtime guards before accessing secrets. Bundler configurations change, and a single misconfigured import can expose everything.

  2. Avoid dynamic RegExp with any user-influenced data: Use string methods (slice, startsWith, indexOf) whenever possible. If you must use RegExp, escape inputs with a utility like escapeRegExp().

  3. Audit yarn.lock / package-lock.json for CVEs: The brace-expansion vulnerability (CVE-2026-14257) in the dependency tree compounds DoS risks. Use npm audit or yarn audit regularly.

  4. Mark server-only modules explicitly: Use package.json "sideEffects" fields, or framework-specific conventions like Next.js server-only imports, to prevent accidental client bundling.

  5. Principle of least privilege for API keys: Even if keys aren't exposed, rotate them regularly and use scoped keys with spending limits where providers support them.


Key Takeaways

  • The addApiKey() method in roll/openai.js had no environment check, meaning any bundler that resolved process.env at build time would inline real OpenAI API keys into client-facing JavaScript.
  • A single typeof window !== 'undefined' guard provides defense-in-depth against secret exposure, even when architectural boundaries fail.
  • **new RegExp(\^${commandType}`, 'i')ingenerateErrorMessage()was unnecessary**—String.slice()` does the same job without any regex attack surface.
  • CVE-2026-14257 in brace-expansion reminds us that DoS vulnerabilities in transitive dependencies amplify application-level ReDoS risks.
  • The safest regex is no regex at all—when you already know the exact prefix to remove, string operations are both safer and faster.

How Orbis AppSec Detected This

  • Source: process.env.OPENAI_SECRET_* environment variables read in roll/openai.js:612 within the addApiKey() method
  • Sink: this.apiKeys array populated with raw secret values, accessible in any execution context including client-side bundles
  • Missing control: No runtime environment check (typeof window) before loading secrets; no new RegExp() input sanitization in generateErrorMessage()
  • CWE: CWE-200 (Exposure of Sensitive Information to an Unauthorized Actor), CWE-1333 (Inefficient Regular Expression Complexity)
  • Fix: Added browser environment guard to prevent key loading in client contexts and replaced dynamic RegExp with String.slice()

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

This case demonstrates how two seemingly minor code patterns—an unguarded process.env read and a dynamic new RegExp()—can create critical security exposure in a production web service. The fixes are minimal (a 1-line environment check and a method swap from replace(RegExp) to slice()), but the security improvement is substantial: API keys can no longer leak to browsers, and the error message handler can no longer be weaponized for denial of service.

When writing Node.js modules that handle secrets, always assume your code might end up somewhere unexpected. Guard accordingly.


References

Frequently Asked Questions

What is API key exposure in client-side JavaScript?

API key exposure occurs when server-side secrets are inadvertently bundled into client-facing JavaScript, allowing anyone with browser developer tools to extract and misuse those credentials.

How do you prevent API key exposure in Node.js?

Use environment checks (e.g., `typeof window !== 'undefined'`) to guard secret-loading code, ensure bundlers tree-shake server-only modules, and never hardcode secrets in files that may be bundled for the browser.

What CWE is API key exposure?

CWE-200: Exposure of Sensitive Information to an Unauthorized Actor.

Is using environment variables enough to prevent key exposure?

No. While environment variables keep secrets out of source code, if the module that reads them is bundled into client-side JavaScript, bundlers like Webpack may inline `process.env` values, exposing them.

Can static analysis detect API key exposure and ReDoS?

Yes. Static analysis tools can flag `process.env` usage in potentially client-bundled code and identify dynamically constructed RegExp patterns that accept user input, both of which are detectable patterns.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #996

Related Articles

critical

How missing authentication checks happen in React route handlers and how to fix it

A critical vulnerability in ManageMembers.jsx and Settings.jsx allowed any user with network access to perform privileged operations like adding, editing, and deleting members without authentication. The fix implements route-level authentication checks using React Router's Navigate component to redirect unauthenticated users to the login page.

high

How denial of service via malformed HTTP header decoding happens in Node.js OpenTelemetry and how to fix it

A high-severity denial of service vulnerability (CVE-2026-59892) was discovered in the @opentelemetry/propagator-jaeger package, where malformed HTTP headers could crash Node.js applications. The fix involved upgrading from version 2.8.0 to 2.9.0, which includes proper input validation for Jaeger trace context headers.

medium

How OAuth token audience bypass happens in Node.js serverless functions and how to fix it

A critical OAuth authentication vulnerability in a Netlify serverless function allowed any valid Google OAuth token—even those issued to completely different applications—to authenticate successfully. The fix adds proper audience (aud) claim verification using Google's tokeninfo endpoint to ensure only tokens issued specifically for this application are accepted.

high

How signature verification bypass happens in Node.js crypto and how to fix it

A high-severity signature verification bypass was discovered in `apps/panel/panel.js` where the `JMkey` variable was passed directly to `Buffer.from(JMkey, 'hex')` without validating its format. An attacker could supply a malformed hex string to cause silent failures or unexpected behavior in the RSA-SHA256 verification process, potentially bypassing signature checks entirely. The fix adds strict hex format validation before processing.

critical

How insecure nonce generation with Math.random() happens in Node.js HTTP Digest authentication and how to fix it

A critical vulnerability was discovered in `lib/cam.js` where the HTTP Digest authentication client nonce (cnonce) was generated using `Math.random().toString(36)` — a cryptographically insecure source of randomness. An attacker observing authentication exchanges could predict future cnonce values and forge valid authentication responses. The fix replaces this with `crypto.randomBytes(4).toString('hex')`, providing cryptographically secure random values.

high

How unauthenticated endpoint exposure happens in Node.js Express and how to fix it

A high-severity vulnerability in the AgenticATODetectionService allowed unauthenticated users to access sensitive agent status data, trigger detection scans, and view security alerts. The fix adds authentication middleware to four critical API endpoints, ensuring only authorized users can access these sensitive operations.