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

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #996

Related Articles

critical

deleteNestedProperty Prototype Pollution via Dot-Notation Path

The `deleteNestedProperty` function in propertyUtils.ts allowed attackers to manipulate JavaScript object prototypes by passing specially crafted dot-notation paths like `__proto__.polluted`. A fix now blocks dangerous keys before processing, preventing prototype pollution attacks that could affect all objects in the application.

high

How Denial of Service via Infinite Loop Happens in JavaScript Dependencies and How to Fix It

CVE-2026-67213 is a high-severity denial of service vulnerability in nanoid before version 5.1.6 that triggers an infinite loop during random ID generation when processing specially crafted input. We upgraded nanoid across the entire dependency tree to patch this flaw and prevent attackers from freezing application threads. This fix ensures that ID generation remains resilient even when handling adversarial input patterns.

high

How Sensitive Data Exposure happens in Zotero plugins and how to fix it

A high-severity data exposure vulnerability in `Zotero.ts` automatically transmitted complete document metadata—including private notes, attachment paths, and tags—to external LLM services without user consent. The fix replaces broad `item.toJSON()` serialization with explicit field selection, sending only essential bibliographic data.

high

How missing dependency update cooldowns happen in GitHub Dependabot configurations and how to fix it

A semgrep scan flagged `.github/dependabot.yml` for lacking a cooldown period, meaning Dependabot would immediately propose updates to brand-new package versions across npm, Bundler, and Docker ecosystems. The fix adds a `cooldown: default-days: 7` block to every `package-ecosystem` entry, forcing a one-week waiting period before newly published releases are considered — reducing exposure to malicious or unstable package drops.

high

How Path Traversal Happens in TensorFlow's Data Service and How to Fix It

TensorFlow's data service dispatcher validated dataset IDs against forward-slash traversal attacks but overlooked backslash characters on non-Windows platforms, allowing attackers to escape the root directory. A targeted fix adds explicit backslash validation across all platforms, closing a high-severity path traversal vulnerability in the snapshot management system.

critical

How Unbounded WebSocket Message Handling Causes Resource Exhaustion in Node.js and How to Fix It

The WebSocketCrossServerAdapter class in a popular Node.js WebSocket library lacked any rate limiting on inbound messages, allowing attackers to flood Redis nodes and WebSocket servers with high-volume traffic. The fix introduces a configurable `rateLimit` option that caps messages per connection per second, preventing resource exhaustion while preserving legitimate functionality.