Back to Blog
high SEVERITY7 min read

How Command Injection Happens in Node.js Child Process Calls and How to Fix It

A Node.js library was vulnerable to command injection through unsafe use of `execSync()` with shell string interpolation in the `index.js` file. By switching to `execFileSync()` with argument arrays, the fix eliminates the ability for attackers to inject shell metacharacters through file paths. This change demonstrates a critical security hardening pattern for any Node.js code that spawns child processes.

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

Answer Summary

This is a command injection vulnerability (CWE-78) in Node.js occurring when `execSync()` uses string interpolation with user-controlled file paths. The vulnerable code concatenated file paths directly into shell commands, allowing attackers to inject shell metacharacters like `;` to execute arbitrary commands. The fix replaces `execSync()` with `execFileSync()` and passes file paths as separate array arguments, preventing shell interpretation of special characters.

Vulnerability at a Glance

cweCWE-78 (Improper Neutralization of Special Elements used in an OS Command)
fixReplace execSync() with execFileSync() and pass arguments as array elements
riskArbitrary command execution with application privileges
languageJavaScript (Node.js)
root causeUsing execSync() with shell string interpolation on user-controlled file paths
vulnerabilityCommand Injection via unsafe child_process.execSync()

How Command Injection Happens in Node.js Child Process Calls and How to Fix It

The Vulnerability in Context

In a Node.js library, a high-severity command injection vulnerability was discovered in index.js at line 8. The vulnerable code was calling execSync() with shell string interpolation on a user-controlled file path parameter, creating an exploit primitive that could allow attackers to execute arbitrary commands. This pattern is particularly dangerous in libraries because vulnerabilities affect all downstream consumers who depend on the package.

The specific problematic code looked like this:

const { execSync } = require('child_process')

function run (script) {
  try {
    const stdout = execSync(`php ${path.join(__dirname, script)}`)
    return stdout.toString()
  } catch (err) {
    throw new Error(`PHP process exited with code ${err.status}`)
  }
}

Notice the backtick string interpolation on line 9: execSync(`php ${path.join(__dirname, script)}`). This approach passes the entire command as a shell string, which means the shell will interpret special characters in the script parameter.

The Vulnerability Explained

Why This Code Is Dangerous

When you use execSync() with a template string, Node.js executes the command through a shell by default. This means any shell metacharacters in the script parameter will be interpreted as shell syntax, not as literal filename characters.

Example attack scenario:

If an attacker calls the vulnerable run() function with this input:

php.run('./test/lib/templates/selfContainedTest.php; rm -rf /')

The actual command executed becomes:

php /full/path/to/test/lib/templates/selfContainedTest.php; rm -rf /

The shell sees the semicolon as a command separator and executes the destructive rm -rf / command with the same privileges as the Node.js application.

Other dangerous metacharacters include:
- | (pipe) - redirect output to another command
- && (AND operator) - execute next command if first succeeds
- || (OR operator) - execute next command if first fails
- ` (backticks) - command substitution
- $() - command substitution
- & (background execution)

Real-World Impact

For a PHP templating library like this one, an attacker could:
1. Inject arbitrary shell commands through the script filename
2. Read sensitive files on the system
3. Modify or delete application data
4. Establish reverse shells for persistent access
5. Pivot to other systems on the network

The vulnerability was classified as HIGH severity because it provides direct code execution with the privileges of the Node.js process.

The Fix

The security patch made two critical changes to eliminate the command injection vulnerability:

Change 1: Replace execSync with execFileSync

-const { execSync } = require('child_process')
+const { execFileSync } = require('child_process')

execFileSync() is fundamentally different from execSync():
- execSync(): Executes a command string through a shell interpreter
- execFileSync(): Directly executes a file without invoking a shell

Change 2: Pass Arguments as Array Elements

 function run (script) {
+  const scriptPath = path.join(__dirname, script)
   try {
-    const stdout = execSync(`php ${path.join(__dirname, script)}`)
+    const stdout = execFileSync('php', [scriptPath])
     return stdout.toString()
   } catch (err) {
     throw new Error(`PHP process exited with code ${err.status}`)
   }
 }

This change is crucial. Instead of:

execSync(`php ${path.join(__dirname, script)}`)  // VULNERABLE: shell interprets the string

The fix uses:

execFileSync('php', [scriptPath])  // SAFE: arguments are passed directly to the executable

By passing the PHP script path as an array element in the second argument, Node.js passes it directly to the PHP executable without shell interpretation. Shell metacharacters become harmless literal characters.

Applied to Both Vulnerable Functions

The same pattern was applied to the runWithData() function:

 function runWithData (template, model) {
   const jsonModel = JSON.stringify(model, circular())

   try {
-    const stdout = execSync(`php ${path.join(__dirname, '/loader.php')}`, {
+    const stdout = execFileSync('php', [path.join(__dirname, '/loader.php')], {
       input: jsonModel
     })
     return stdout.toString()

Verification: The New Test Case

The fix includes a new security test that validates the vulnerability is resolved:

test.serial('Shell metacharacters in the script path are not executed as shell commands', async t => {
  try {
    await php.run('./test/lib/templates/selfContainedTest.php; echo pwned')
  } catch (e) {
    t.true(e.message.includes('PHP process exited with code'))
  }
})

This test attempts to inject the command ; echo pwned through the script path. With the vulnerable code, this would execute the echo command. With the fix, the semicolon is treated as a literal filename character, PHP fails to find a file with that name, and an error is thrown as expected.

Prevention & Best Practices

1. Always Use execFileSync() or execFile() Instead of execSync() or exec()

When you need to execute a program with parameters:

// ❌ UNSAFE
execSync(`program ${userInput}`)

// ✅ SAFE
execFileSync('program', [userInput])

2. Never Use String Interpolation for Command Arguments

// ❌ UNSAFE
execSync(`php script.php --name="${userName}"`)

// ✅ SAFE
execFileSync('php', ['script.php', '--name=' + userName])
// or with proper escaping if needed:
execFileSync('php', ['script.php', `--name=${userName}`])

3. Avoid shell: true Option

// ❌ UNSAFE
execSync('php script.php', { shell: true })

// ✅ SAFE
execFileSync('php', ['script.php'])

4. Validate and Sanitize Input When Possible

Even with execFileSync(), validate input:

function run(script) {
  // Validate that script is a relative path to a PHP file
  if (!script.endsWith('.php') || script.includes('..')) {
    throw new Error('Invalid script path')
  }
  const scriptPath = path.join(__dirname, script)
  const stdout = execFileSync('php', [scriptPath])
  return stdout.toString()
}

5. Use Static Analysis Tools

Semgrep, ESLint with security plugins, and other static analysis tools can detect these patterns:

semgrep --config=p/security-audit index.js

6. Reference Security Standards

  • CWE-78: OS Command Injection - https://cwe.mitre.org/data/definitions/78.html
  • OWASP Command Injection: https://owasp.org/www-community/attacks/Command_Injection

Key Takeaways

  • Never use execSync() with string interpolation on user-controlled input. The vulnerable pattern execSync(`command ${userInput}`) is inherently unsafe because the shell interprets metacharacters.

  • execFileSync() with array arguments is the correct pattern for spawning child processes in Node.js. This bypasses the shell and treats all arguments as literal values, eliminating command injection.

  • Shell metacharacters (;, |, &&, etc.) in file paths become harmless when using execFileSync() because they're never passed to a shell interpreter.

  • The path.join() function alone is insufficient for security. While it normalizes paths, it doesn't prevent injection attacks when the result is interpolated into a shell command.

  • This vulnerability affects all downstream consumers of the library. Security issues in dependencies can compromise applications that use them, making proactive fixes essential for library maintainers.

How Orbis AppSec Detected This

Source: The script parameter passed to the run() function (user-controlled input from function arguments)

Sink: The execSync() call at index.js:9 where the script parameter is interpolated into a shell command string

Missing control: No validation that the script parameter contains only safe characters, and the use of execSync() with shell string interpolation instead of execFileSync() with argument arrays

CWE: CWE-78 - Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')

Fix: Replaced execSync() with execFileSync(), moved the path joining outside the command string, and passed the script path as an array argument to prevent 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

Command injection through unsafe child process calls is a critical vulnerability that can lead to complete system compromise. The fix in this case demonstrates the importance of using the right API for the job: execFileSync() with array arguments is the secure way to spawn child processes in Node.js when you need to pass dynamic parameters.

By understanding how shell metacharacters can be exploited through string interpolation, developers can write more secure code. This vulnerability also highlights the value of static analysis tools like Semgrep in catching dangerous patterns before they reach production.

If you maintain a Node.js library or application that spawns child processes, audit your codebase now for similar patterns. Replace execSync() and exec() calls with their safer counterparts, and always pass dynamic arguments as array elements rather than interpolating them into command strings.


References

Frequently Asked Questions

What is command injection in Node.js?

Command injection occurs when user-controlled input is passed to shell commands without proper sanitization. In Node.js, using `execSync()` with string interpolation allows attackers to inject shell metacharacters (like `;`, `|`, `&&`) to execute arbitrary commands.

How do you prevent command injection in Node.js?

Use `execFileSync()` or `execFile()` instead of `execSync()`, pass arguments as array elements rather than string concatenation, and avoid shell=true. Additionally, validate and sanitize all user input before passing it to child process functions.

What CWE is command injection?

Command injection is CWE-78: "Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')". It's a critical vulnerability that can lead to complete system compromise.

Is input validation enough to prevent command injection in execSync()?

No. While input validation helps, the safest approach is to use `execFileSync()` which bypasses the shell entirely, making shell metacharacters harmless. Validation alone is insufficient because it's difficult to whitelist all safe characters.

Can static analysis detect command injection in Node.js?

Yes. Tools like Semgrep can detect patterns where `child_process` functions are called with tainted data or string interpolation. Semgrep's rule `javascript.lang.security.detect-child-process.detect-child-process` specifically identifies these vulnerable patterns.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #177

Related Articles

critical

How Command Injection happens in Node.js shell-quote and how to fix it

A critical command injection vulnerability (CVE-2026-9277) was discovered in shell-quote versions prior to 1.8.4, where unescaped line terminators allowed attackers to inject arbitrary shell commands through crafted input strings. The fix pins shell-quote to version 1.9.0 via a `package.json` overrides directive in the FabricExample project, ensuring all transitive dependencies resolve to the patched version. Left unaddressed, this vulnerability could have allowed arbitrary code execution on any

high

How Command Injection happens in Node.js child_process calls and how to fix it

A high-severity command injection vulnerability was discovered in `tools/utils/lang/helpers.ts` where the `prettier()` function passed a user-controllable `fileName` argument directly into a shell command string via `exec()`. By replacing `exec()` with `execFile()` and passing arguments as an array, the fix eliminates shell interpolation entirely, preventing attackers from injecting arbitrary shell commands through malicious filenames.

high

How Command Injection Happens in Node.js child_process and How to Fix It

A high-severity command injection vulnerability was discovered in `scripts/common.js` where the `exec()` function used `execSync()` with unsanitized input, allowing potential command injection attacks. The fix replaces `execSync()` with `execFileSync()` and separates command arguments into an array, preventing shell metacharacter interpretation. This defensive hardening removes an exploit primitive that could be chained with other weaknesses by automated attack tools.

high

How Command Injection via child_process happens in Node.js and how to fix it

A high-severity command injection vulnerability was discovered in `bin/init.mjs` where the `shallowClone` function passed a user-controllable `ref` parameter directly to `execSync` shell commands. This could allow attackers to execute arbitrary system commands by crafting malicious git reference names. The fix implements strict input validation and replaces `execSync` with `execFileSync` to eliminate shell interpretation entirely.

critical

How Command Injection happens in Python subprocess and how to fix it

A critical command injection vulnerability was discovered in the `open_directory` method of `src/jm_view_server/app.py`, where user-controlled path input was passed directly into a shell command via `subprocess.Popen`. By switching from string-based shell execution to a list-based argument format, the fix eliminates the ability for attackers to inject malicious shell commands through crafted directory paths.

high

How Unauthenticated Denial of Service happens in React Router and how to fix it

CVE-2026-55685 is a high-severity Denial of Service vulnerability in React Router's `@remix-run/server-runtime` that allows unauthenticated attackers to exhaust server resources by sending crafted requests to the manifest endpoint. The fix upgrades `react-router` from version 7.17.0 to 7.18.0, which tightens handling of untrusted input in route matching logic. Developers using any React Router 7.x application with server-side rendering should apply this patch immediately.