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
•Technically reviewed by Anupam Mediratta•Published September 1, 2026•Reviewed 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

ARTIFICIAL_ANALYSIS_API_KEY: Unvalidated os.getenv in Model Sync

A model-catalogue sync utility read the `ARTIFICIAL_ANALYSIS_API_KEY` environment variable with a bare `os.getenv()` call and passed whatever it found straight into `fetch_intelligence_from_api()`. Any malformed value — a trailing newline from an `--env-file`, a whitespace-only placeholder, or a pasted `Bearer …` prefix — was treated as a valid credential and forwarded into an outbound HTTP request, where it could surface in CI error output. The fix normalizes the value, validates it against a s

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.