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 `packages/runner/src/main.js` where the `child_process.spawn()` function accepted an unvalidated `argv` array parameter. An attacker could potentially inject malicious arguments to execute arbitrary commands. The fix adds strict type validation for the `argv` array and explicitly disables shell execution to prevent command injection attacks.

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

Answer Summary

This is a command injection vulnerability (CWE-78) in Node.js where `child_process.spawn()` receives an unvalidated `argv` parameter that could contain malicious input. The fix validates that `argv` is an array of strings before use and sets `shell: false` explicitly to prevent shell interpretation of special characters. These two changes eliminate the command injection attack surface in the runner module.

Vulnerability at a Glance

cweCWE-78
fixValidate argv is string array and set shell: false
riskArbitrary command execution on the host system
languageJavaScript (Node.js)
root causeUnvalidated argv array passed directly to child_process.spawn()
vulnerabilityCommand Injection via child_process

Introduction

In the packages/runner/src/main.js file of a Node.js runner application, we discovered a high-severity command injection vulnerability at line 82. The run() function accepts a destructured object containing argv—an array of command-line arguments—and passes it directly to child_process.spawn() without any validation.

The vulnerable code pattern looked like this:

let nwProcess = child_process.spawn(nwExe, [...[srcDir], ...argv], {
  stdio: "inherit",
});

This matters because the argv parameter flows from function arguments into a system command execution context. If any upstream code passes user-controllable data into this function, an attacker could inject malicious arguments or, depending on the environment, exploit shell metacharacters to execute arbitrary commands.

The Vulnerability Explained

The run() function in main.js is designed to spawn an NW.js (Node-WebKit) process with specific arguments. It receives a configuration object with properties including version, flavor, platform, arch, srcDir, cacheDir, and critically, argv.

Here's the vulnerable code before the fix:

async function run({
  version,
  flavor,
  platform,
  arch,
  srcDir,
  cacheDir,
  argv,
}) {
  // ... path validation for nwExe ...

  let nwProcess = child_process.spawn(nwExe, [...[srcDir], ...argv], {
    stdio: "inherit",
  });

Why This Is Dangerous

  1. No Type Validation: The code assumes argv is an array of strings, but never verifies this. If argv contained objects, functions, or specially crafted values, unexpected behavior could occur.

  2. No Shell Flag Specified: While child_process.spawn() defaults to shell: false, explicitly omitting this setting leaves the security posture ambiguous and makes the code vulnerable to future changes or misconfigurations.

  3. Direct Parameter Passthrough: The spread operator (...argv) passes all elements directly to the spawned process without sanitization.

Attack Scenario

Consider if an upstream API endpoint allowed users to specify runner options:

// Hypothetical vulnerable upstream code
app.post('/run', (req, res) => {
  run({
    version: req.body.version,
    argv: req.body.argv,  // User-controlled!
    // ...
  });
});

An attacker could send:

{
  "argv": ["--malicious-flag", "; rm -rf /", "--another-arg"]
}

While spawn() without shell mode wouldn't interpret the semicolon, if the code were ever changed to use shell: true, or if argv contained non-string values that coerced unexpectedly, command injection would be possible.

The Fix

The fix implements two critical security controls in packages/runner/src/main.js:

Change 1: Input Validation (Line 79-81)

if (!Array.isArray(argv) || !argv.every((arg) => typeof arg === "string")) {
  throw new Error("Invalid argv: expected an array of strings");
}

This validation ensures:
- argv is definitely an array (not null, undefined, or an object)
- Every element in argv is a string (not objects, numbers, or functions that could cause unexpected behavior)

Change 2: Explicit Shell Disable (Line 87)

let nwProcess = child_process.spawn(nwExe, [...[srcDir], ...argv], {
  stdio: "inherit",
  shell: false,  // Explicitly added
});

Setting shell: false explicitly:
- Documents the security intent
- Prevents accidental shell interpretation if defaults ever change
- Ensures special characters like ;, |, && are treated as literal arguments

Before and After Comparison

Before:

let nwProcess = child_process.spawn(nwExe, [...[srcDir], ...argv], {
  stdio: "inherit",
});

After:

if (!Array.isArray(argv) || !argv.every((arg) => typeof arg === "string")) {
  throw new Error("Invalid argv: expected an array of strings");
}

let nwProcess = child_process.spawn(nwExe, [...[srcDir], ...argv], {
  stdio: "inherit",
  shell: false,
});

Prevention & Best Practices

1. Always Validate Input Types

Before passing any data to child_process functions, validate:
- The data type matches expectations
- The content conforms to expected patterns
- No unexpected characters or values are present

function validateArgs(args) {
  if (!Array.isArray(args)) {
    throw new TypeError('Arguments must be an array');
  }
  return args.every(arg => 
    typeof arg === 'string' && 
    /^[\w\-\.\/]+$/.test(arg)  // Allowlist safe characters
  );
}

2. Never Use shell: true with User Input

When shell: true is set, special characters are interpreted by the shell:

// DANGEROUS - Never do this with user input
child_process.spawn(cmd, args, { shell: true });

// SAFER - Shell interpretation disabled
child_process.spawn(cmd, args, { shell: false });

3. Use execFile Instead of exec

child_process.execFile() doesn't spawn a shell by default, making it safer than exec():

// Preferred for running specific executables
child_process.execFile('/path/to/binary', ['arg1', 'arg2']);

4. Implement Allowlists for Arguments

If possible, validate arguments against a known-good list:

const ALLOWED_FLAGS = ['--verbose', '--debug', '--quiet'];
const safeArgs = argv.filter(arg => ALLOWED_FLAGS.includes(arg));

Key Takeaways

  • Always validate argv arrays before passing to child_process.spawn() in Node.js runner applications
  • The run() function in main.js now rejects non-string array elements, preventing type confusion attacks
  • Explicit shell: false documents security intent and prevents future regressions
  • Defense in depth combines input validation with secure spawn options for robust protection
  • Private applications still need hardening—this vulnerability could be chained with other weaknesses by automated exploit tools

How Orbis AppSec Detected This

  • Source: Function parameter argv passed to the run() function in packages/runner/src/main.js
  • Sink: child_process.spawn(nwExe, [...[srcDir], ...argv], {...}) at line 82
  • Missing control: No validation that argv is an array of strings; no explicit shell: false setting
  • CWE: CWE-78 (Improper Neutralization of Special Elements used in an OS Command)
  • Fix: Added type validation for argv array and explicitly set shell: false to prevent command injection

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 packages/runner/src/main.js demonstrates why input validation is critical even in private applications. The unvalidated argv parameter flowing directly into child_process.spawn() created an exploit primitive that could be chained with other vulnerabilities.

The two-line fix—validating argv as a string array and explicitly setting shell: false—eliminates this attack surface while maintaining the function's intended behavior. When working with child_process in Node.js, always validate input types, use explicit security settings, and consider allowlisting valid arguments.

References

Frequently Asked Questions

What is command injection in child_process?

Command injection occurs when untrusted input is passed to system command execution functions like child_process.spawn(), allowing attackers to execute arbitrary commands on the server.

How do you prevent command injection in Node.js?

Validate all input types before passing to child_process, use shell: false to disable shell interpretation, and sanitize or allowlist command arguments.

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 setting shell: false enough to prevent command injection?

While shell: false prevents shell metacharacter interpretation, you should also validate input types and sanitize arguments for defense in depth.

Can static analysis detect command injection?

Yes, tools like Semgrep can detect patterns where user-controllable input flows into child_process functions without proper validation.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #1645

Related Articles

critical

How Command Injection Vulnerabilities Happen in Python Subprocess Calls and How to Fix Them

A critical command injection vulnerability was discovered in `src/unused/server/fft.py` where external binaries like `oggenc` and `cocoa_text` were executed with file path parameters that could be manipulated by user input. Although `shell=False` was used, the lack of input validation allowed attackers to potentially trigger processing of arbitrary files or cause denial of service. This fix implements proper path validation to prevent exploitation.

critical

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.

high

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

A command injection vulnerability was discovered in `scripts/install.js` where user-controllable input was passed to `child_process.execSync()` through string interpolation. This high-severity issue could allow attackers to execute arbitrary shell commands by crafting malicious package file paths. The fix replaces `execSync()` with `execFileSync()`, which bypasses the shell entirely and treats arguments as literal values.

critical

How Arbitrary Code Execution Via Command Injection happens in Node.js and how to fix it

A critical arbitrary code execution flaw in the `shell-quote` npm package (CVE-2026-9277) allowed attackers to break out of shell quoting using unescaped Unicode line terminator characters, turning ordinary command-line arguments into injected shell commands. The fix locks `shell-quote` to the patched `1.8.4` release via a `resolutions` override in `package.json`/`yarn.lock`, closing off a transitive dependency path that could otherwise pull in a vulnerable version.

critical

How command injection happens in Kotlin/Android and how to fix it

V2rayNG's RootShell.kt built root shell commands by concatenating an unescaped file path directly into a string passed to `su -c`, creating a critical command injection risk (CWE-78). The fix restricts the `exec()` API to internal use only and single-quote-escapes the file path before it ever reaches the root shell.

high

How SQL Injection via Template Literals happens in Node.js and how to fix it

A high-severity SQL injection vulnerability was discovered in server-agents/common/src/search/schema.ts where the `insertRowsBatch` function constructed SQL queries using JavaScript template literals with dynamic input. The fix replaced the vulnerable `db.exec()` call with parameterized queries using `db.query().run()`, eliminating the injection risk in the full-text search merge operation.