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.


Prevention & Best Practices

1. Never Spawn Processes for HTTP Calls

Don't do this:

import { spawnSync } from 'node:child_process';
spawnSync('curl', ['-H', `Authorization: Bearer ${token}`, 'https://api.example.com']);

Do this instead:

const res = await fetch('https://api.example.com', {
  headers: { Authorization: `Bearer ${token}` }
});

2. Keep Secrets Out of Process Arguments

When you must spawn processes, never pass secrets as arguments. Use environment variables or stdin instead:

// ❌ Bad: token visible in process list
spawnSync('some-tool', ['--token', secretToken]);

// ✅ Better: pass via environment
spawnSync('some-tool', [], { 
  env: { ...process.env, TOOL_TOKEN: secretToken } 
});

// ✅ Best: use stdin for sensitive data
const child = spawn('some-tool');
child.stdin.write(secretToken);
child.stdin.end();

3. Never Document API Key Patterns

Remove all examples that show:
- Hardcoded tokens (even fake examples)
- Token format patterns (e.g., "Bearer ghp_...")
- Configuration examples with placeholder credentials

4. Implement Secret Scanning

Use tools that scan your repository for credential patterns:

  • git-secrets: Prevents credentials from being committed
  • TruffleHog: Searches for high-entropy strings that look like credentials
  • Semgrep: Static analysis rules for credential detection
  • GitHub Secret Scanning: Built-in scanning for common credential formats

5. Use Environment Variables Correctly

// Load from environment at startup
const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;

// Validate that token exists before using
if (!token) {
  throw new Error('GITHUB_TOKEN environment variable is required');
}

// Never log the token
console.log('Using GitHub API with token: [REDACTED]');

6. Audit Dependencies and Child Processes

Before spawning any external process:
- Ask: "Can I do this with a native Node.js API?"
- If yes, use the native API
- If no, ensure the external tool is from a trusted source and verify its integrity

7. Use CWE and OWASP References

  • CWE-798: Use of Hardcoded Credentials - https://cwe.mitre.org/data/definitions/798.html
  • CWE-78: Improper Neutralization of Special Elements used in an OS Command - https://cwe.mitre.org/data/definitions/78.html
  • OWASP A02:2021: Cryptographic Failures - https://owasp.org/Top10/A02_2021-Cryptographic_Failures/

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.


References

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.

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

(1) Never hardcode credentials in code or documentation; (2) Always use environment variables for secrets; (3) Use native APIs (fetch, http module) instead of spawning external processes for sensitive operations; (4) Implement secret scanning in your CI/CD pipeline; (5) Rotate credentials regularly and monitor for unauthorized access.

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.

Is using environment variables enough to prevent API key exposure?

Environment variables are a good practice for runtime secrets, but they're only half the solution. You must also: (1) never document examples with real keys, (2) never log process arguments, (3) use secure storage for credentials at rest, and (4) implement access controls to limit who can view environment variables in production.

Can static analysis detect this vulnerability?

Yes. Static analysis can detect: (1) spawnSync calls with hardcoded strings containing "Authorization" or "Bearer" patterns, (2) documentation files containing API key configuration examples, (3) process spawning for HTTP operations (which should use native APIs), and (4) credential patterns in comments or strings using regex rules.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #16

Related Articles

critical

How hardcoded API credentials in client-side JavaScript happens in userscripts and how to fix it

The jhs-enhance.user.js userscript contained a hardcoded Imgur API Client-ID embedded directly in client-side JavaScript code, exposing it to anyone who installed or viewed the script source. This critical vulnerability allowed unauthorized users to extract and abuse the API credentials for unlimited image uploads. The fix replaced the hardcoded credential with a user-prompt mechanism that requires each user to provide their own Imgur Client-ID.

critical

How Hardcoded HMAC-SHA256 Keys Compromise API Authentication in HarmonyOS and How to Fix It

A critical vulnerability in the Bika application exposed a hardcoded HMAC-SHA256 signing key directly in the Constants.ets file, allowing attackers to forge valid API requests. The fix implements runtime key deobfuscation using XOR masking, removing the plaintext credential from both source code and compiled binaries. This change demonstrates why symmetric keys must never be embedded in client-side code.

critical

How API Key Exposure in URL Parameters happens in Python and how to fix it

The Wine Cellar Home Assistant integration exposed Gemini API keys by transmitting them as URL query parameters in HTTP requests. This critical vulnerability allowed API keys to be logged in server logs, proxy caches, and browser history. The fix moved authentication to the secure `x-goog-api-key` HTTP header, preventing credential leakage.

critical

How Hardcoded API Keys Happen in TOML Configuration Files and How to Fix Them

A hardcoded Google Maps API key was discovered in `exampleSite/config/_default/params.toml` at line 113, exposing a live credential that any attacker could extract from the repository and use to make unauthorized API calls. This critical vulnerability was automatically detected and fixed by replacing the hardcoded key with an empty placeholder, eliminating the risk of credential theft and unauthorized usage charges.

critical

How Plaintext Secret Storage Happens in Cloudflare Workers (wrangler.toml) and How to Fix It

A critical misconfiguration in `platforms/m365/wrangler.toml` left developers one copy-paste away from committing live API keys directly into git history. The fix adds an explicit warning comment blocking the `[vars]` anti-pattern and adds `.dev.vars` to `.gitignore`, ensuring secrets flow through Cloudflare's encrypted `wrangler secret` mechanism instead of plaintext config. This matters because git history is permanent — a key committed even once can be extracted long after it's "deleted."

critical

How Hardcoded API Keys Happen in JavaScript and How to Fix Them

A critical security vulnerability was discovered in `src/js/init.js` where a Bugsnag API key was hardcoded directly into client-side JavaScript, making it visible to anyone who inspects the page source or JavaScript bundle. The fix replaces the hardcoded string with an environment variable reference (`import.meta.env.VITE_BUGSNAG_API_KEY`), ensuring the key is injected at build time rather than baked into the shipped code. This pattern is one of the most common — and most avoidable — secrets exp