Back to Blog
critical SEVERITY8 min read

How API Key Exposure and Unsafe Process Spawning Happens in Node.js Scripts and How to Fix It

A critical security vulnerability in the `scripts/close-issues.mjs` file exposed API key patterns in documentation and used unsafe `spawnSync` calls to execute curl commands. The fix replaces dangerous process spawning with native `fetch()` API calls and removes sensitive configuration examples from documentation, eliminating both credential exposure and command injection risks.

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

Answer Summary

This vulnerability combines two critical security flaws in Node.js: (1) API key configuration patterns exposed in documentation (CWE-798: Use of Hardcoded Credentials), and (2) unsafe use of `spawnSync()` to execute shell commands with sensitive data (CWE-78: Improper Neutralization of Special Elements used in an OS Command). The fix replaces `spawnSync('curl', [...])` calls with native `fetch()` API, which eliminates the shell execution layer and prevents credential leakage through process arguments visible to other system processes.

Vulnerability at a Glance

cweCWE-798 (Hardcoded Credentials), CWE-78 (OS Command Injection)
fixReplace spawnSync with native fetch() API; remove sensitive examples from documentation
riskAttackers could extract API key patterns from documentation and exploit process argument visibility to intercept credentials
languageJavaScript (Node.js)
root causeUsing spawnSync to execute curl with unencrypted tokens in process arguments; API key patterns documented in public repo
vulnerabilityAPI Key Exposure + Unsafe Process Spawning

How API Key Exposure and Unsafe Process Spawning Happens in Node.js Scripts and How to Fix It

Introduction

In the scripts/close-issues.mjs file of a Node.js project, a critical security vulnerability combined two dangerous patterns: API key configuration examples were documented in public repository files, and the script used spawnSync() to execute curl commands with sensitive authentication tokens passed as command-line arguments.

The specific problem was in the post() and close() functions (lines 97-108 in the vulnerable version), where the code looked like this:

const r = spawnSync('curl', [
  '-sS', '-X', 'POST',
  '-H', `Authorization: Bearer ${token}`,
  '-H', 'Accept: application/vnd.github+json',
  'https://api.github.com/repos/HaloTech-Co-Ltd/hk2/issues/' + issue + '/comments',
  '-H', 'Content-Type: application/json',
  '-d', JSON.stringify({ body }),
], { encoding: 'utf8' });

This pattern is dangerous because:
1. Process argument visibility: The ${token} variable is passed as a command-line argument to the spawned curl process, making it visible to other processes on the system
2. Documentation exposure: The PR description indicates API key patterns were documented in README_zh.md and script comments
3. Loss of control: Spawning external processes for HTTP operations means losing direct control over error handling and security properties

For downstream consumers of this Node.js library, this vulnerability meant that anyone using this script could inadvertently expose credentials through process inspection or documentation leakage.


The Vulnerability Explained

What Makes This Dangerous?

When you use spawnSync('curl', [args]) with sensitive data in the arguments array, you create multiple security problems:

Problem 1: Process Argument Visibility

On Linux/Unix systems, any process can inspect /proc/[pid]/cmdline to see the arguments of running processes. This means:

# Another user or attacker can see:
$ cat /proc/12345/cmdline
curl-sS-XPOSTAuthorization: Bearer ghp_1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q...

The API token is now visible to any process running on the system. On Windows, similar information is available through the Windows API.

Problem 2: Logging and Monitoring

When spawnSync fails, error messages might include the command that was executed:

if (r.status !== 0) throw new Error(`comment #${issue} failed: ${r.stderr}`);

If the curl command itself appears in error messages or logs, the token is logged.

Problem 3: Documentation Exposure

The PR description explicitly mentions that API key configuration patterns were exposed in documentation files. This teaches attackers the exact patterns to look for when searching for credentials in repositories.

Attack Scenario

An attacker could:

  1. Clone the public repository
  2. Search documentation files for API key patterns (e.g., "Bearer ghp_" for GitHub tokens)
  3. Monitor the system where the script runs using tools like ps or /proc inspection
  4. Capture the API token from process arguments
  5. Use the stolen token to make unauthorized API calls to the GitHub repository

The combination of documented patterns + process argument visibility creates a critical vulnerability.


The Fix

What Changed?

The fix makes two key changes:

Change 1: Replace spawnSync with native fetch()

Instead of spawning curl as a child process, the code now uses Node.js's native fetch() API (available in Node.js 18+):

Before:

const r = spawnSync('curl', [
  '-sS', '-X', 'POST',
  '-H', `Authorization: Bearer ${token}`,
  '-H', 'Accept: application/vnd.github+json',
  'https://api.github.com/repos/HaloTech-Co-Ltd/hk2/issues/' + issue + '/comments',
  '-H', 'Content-Type: application/json',
  '-d', JSON.stringify({ body }),
], { encoding: 'utf8' });
if (r.status !== 0) throw new Error(`comment #${issue} failed: ${r.stderr}`);
const parsed = JSON.parse(r.stdout);
if (!parsed.id) throw new Error(`comment #${issue} failed: ${r.stdout.slice(0, 300)}`);

After:

const res = await fetch(`https://api.github.com/repos/${REPO}/issues/${issue}/comments`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${token}`,
    Accept: 'application/vnd.github+json',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ body }),
});
const text = await res.text();
const parsed = JSON.parse(text);
if (!res.ok || !parsed.id) throw new Error(`comment #${issue} failed: ${text.slice(0, 300)}`);
console.log(`commented #${issue}: ${parsed.html_url}`);

Why this is better:

  1. No process spawning: The token stays in memory within the Node.js process; it's never passed as a command-line argument
  2. Better error handling: fetch() provides direct access to HTTP status codes and response bodies
  3. No shell interpretation: There's no shell layer that could misinterpret special characters
  4. Cleaner code: The HTTP logic is explicit and easier to audit

Change 2: Remove the unsafe import

The vulnerable version imported spawnSync at the top of the file:

import { spawnSync } from 'node:child_process';

This import was completely removed, eliminating the dependency on child process spawning entirely.

Security Improvements

  1. Credential Protection: API tokens are no longer visible in process arguments
  2. Reduced Attack Surface: Removing child_process usage eliminates an entire class of command injection vulnerabilities
  3. Better Auditability: HTTP calls using fetch are easier to review and understand
  4. Process Isolation: The application no longer depends on external tools (curl), reducing supply chain risk

The same fix was applied to both the post() function (which comments on issues) and the close() function (which closes issues), ensuring consistent security across all API interactions.


Key Takeaways

  • Never pass API tokens as command-line arguments to spawned processes — they're visible to other processes on the system via /proc inspection
  • Replace spawnSync/spawn for HTTP calls with native fetch() — it keeps credentials in memory and eliminates shell interpretation risks
  • Remove API key configuration examples from documentation — even fake examples teach attackers what to search for
  • The combination of documented patterns + unsafe process spawning created a critical vulnerability — fixing one without the other would leave the application partially exposed
  • Use static analysis and secret scanning in CI/CD — automated tools can catch these patterns before they're committed to your repository

How Orbis AppSec Detected This

Source: GitHub repository documentation files (README_zh.md) and script comments containing API key configuration patterns; HTTP requests in scripts/close-issues.mjs using sensitive authentication tokens.

Sink: spawnSync('curl', [...]) calls at lines 97-108 in the vulnerable version of scripts/close-issues.mjs, where the Authorization: Bearer ${token} header is passed as a command-line argument to a child process.

Missing control:
- No validation preventing sensitive data from being passed as process arguments
- No use of native HTTP APIs (fetch, http module) for HTTP operations
- No documentation review to remove API key patterns
- No environment variable isolation for secrets passed to child processes

CWE:
- CWE-798: Use of Hardcoded Credentials
- CWE-78: Improper Neutralization of Special Elements used in an OS Command

Fix: Replace all spawnSync('curl', [...]) calls with native fetch() API calls, remove the child_process import, and eliminate API key configuration examples from documentation.

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 vulnerability in scripts/close-issues.mjs demonstrates a critical lesson: security is about layers, and removing even one layer can be catastrophic. By combining API key exposure in documentation with unsafe process spawning, the application created multiple attack vectors that an attacker could chain together.

The fix is straightforward: use native APIs for HTTP calls, keep secrets out of process arguments, and remove sensitive patterns from documentation. For Node.js developers, this means:

  1. Prefer fetch() or the http module over spawning curl
  2. Never pass secrets as command-line arguments
  3. Use environment variables for configuration, but don't document the values
  4. Implement secret scanning in your CI/CD pipeline
  5. Regularly audit your code for these patterns

By following these practices, you'll eliminate entire classes of vulnerabilities in your Node.js applications and libraries.


Prevention and further reading

Frequently Asked Questions

Why is spawnSync with curl dangerous for API calls?

When you pass sensitive data like API tokens as command-line arguments to spawnSync, those arguments are visible to other processes via `/proc/[pid]/cmdline` on Linux or similar mechanisms on other OS. Additionally, the child process (curl) runs in a separate context where error handling is less controlled. Native fetch() keeps credentials in memory within the Node.js process and provides better error handling.

What CWE categories apply to this vulnerability?

CWE-798 (Use of Hardcoded Credentials) for the documented API key patterns, and CWE-78 (Improper Neutralization of Special Elements used in an OS Command) for the unsafe process spawning pattern.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #16

Related Articles

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.

critical

How Hardcoded API Keys in WASM Modules Happen in KAP and How to Fix Them

A critical security vulnerability in `wasm/kap/standard-lib/fhelp-impl.kap` exposed hardcoded Gemini API keys directly in source code distributed to end users via WASM modules. The fix replaces the embedded credential with secure environment variable retrieval, preventing credential extraction through browser developer tools or binary inspection.

critical

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.

high

How Insufficiently Protected Credentials happens in Node.js and how to fix it

A regex-based guard in `skills/xmemo/scripts/xmemo-skill.mjs` was supposed to block sensitive credentials from being passed as CLI flags, but it only matched a handful of keyword patterns—missing `password`, `client-secret`, `access-token`, `xmemo-key`, and more. The fix expands the blocklist regex to cover these additional credential patterns, closing a gap that could let secrets end up in shell history and process listings.