Back to Blog
high SEVERITY6 min read

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

A high-severity command injection vulnerability in `gateway-workflow-dispatcher-v2.js` allowed arbitrary command execution through unsanitized input passed to `execSync`. The fix replaces `execSync` with `execFileSync`, eliminating shell interpretation and preventing attackers from injecting malicious commands through the `eventText` parameter.

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

Answer Summary

CVE-2026-56876 is a command injection vulnerability (CWE-78) in a Node.js workflow dispatcher that used `execSync` with string concatenation at line 568 of `gateway-workflow-dispatcher-v2.js`. The vulnerability allowed arbitrary command execution when attacker-controlled `eventText` contained shell metacharacters. The fix replaces `execSync` with `execFileSync`, passing arguments as an array instead of a shell string, which prevents shell interpretation entirely.

Vulnerability at a Glance

cweCWE-78 (OS Command Injection)
fixReplace execSync with execFileSync using array-based argument passing
riskArbitrary command execution on the host system
languageJavaScript (Node.js)
root causeUsing execSync with string concatenation for shell command construction
vulnerabilityCommand Injection

Introduction

In the gateway-workflow-dispatcher-v2.js file, a workflow event dispatching system was quietly harboring a dangerous pattern: the execSync function from Node.js's child_process module was being used with string concatenation to build shell commands. At line 568, the code constructed a command like this:

const result = execSync(
  'openclaw system event --mode now --json --text ' + JSON.stringify(eventText),
  { timeout: 15000, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] }
);

While JSON.stringify might seem like protection, it doesn't prevent shell metacharacter injection. An attacker controlling eventText could break out of the intended command structure and execute arbitrary system commands. This is particularly dangerous in a workflow dispatcher that handles event-driven automation—compromising this component could give attackers control over downstream business processes.

The Vulnerability Explained

The vulnerable code at lines 564-571 in gateway-workflow-dispatcher-v2.js used execSync to execute the openclaw CLI tool:

try {
  const result = execSync(
    'openclaw system event --mode now --json --text ' + JSON.stringify(eventText),
    { timeout: 15000, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] }
  );
  const parsed = JSON.parse(result.trim());
  // ...
}

The critical flaw: execSync spawns a shell (/bin/sh on Unix, cmd.exe on Windows) and passes the entire string to that shell for interpretation. Even with JSON.stringify, an attacker could craft eventText containing shell metacharacters that escape the quoted context.

How the Attack Works

Consider what happens when eventText contains:

"; rm -rf /important/data; echo "

The shell sees:

openclaw system event --mode now --json --text "; rm -rf /important/data; echo "

The semicolons terminate the intended command and start new ones. The JSON.stringify actually helps the attacker by providing clean quoting boundaries to escape from.

In the gateway-workflow-dispatcher-v2.js context, this is especially severe because:
- The GatewayWorkflowDispatcherV2 class handles workflow orchestration
- Compromised event dispatch could propagate to downstream systems
- The 15-second timeout provides ample window for command execution
- The stdio: ['ignore', 'pipe', 'pipe'] configuration still allows command output capture

Real-World Impact

An attacker with control over workflow event data could:
- Exfiltrate sensitive workflow configurations
- Modify budget enforcement rules (the file imports createBudgetEnforcement)
- Pivot to the PostgreSQL database via the Pool connection
- Disrupt business operations by corrupting workflow state

The Fix

The patch makes a surgical but critical change: replacing execSync with execFileSync and restructuring how arguments are passed.

Before (Vulnerable)

const { execSync } = require('child_process');
// ...
const result = execSync(
  'openclaw system event --mode now --json --text ' + JSON.stringify(eventText),
  { timeout: 15000, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] }
);

After (Fixed)

const { execFileSync } = require('child_process');
// ...
const result = execFileSync(
  'openclaw',
  ['system', 'event', '--mode', 'now', '--json', '--text', eventText],
  { timeout: 15000, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] }
);

Why This Fixes the Vulnerability

Aspect execSync (Vulnerable) execFileSync (Fixed)
Shell spawned Yes (/bin/sh or cmd.exe) No (direct execution)
Argument passing Single string, shell-parsed Array of strings, no shell parsing
Metacharacter risk High—shell interprets everything None—arguments passed directly to kernel
Injection vector String concatenation Not possible—array elements are separate

The execFileSync function directly executes the binary at the specified path with the provided arguments array. There's no shell to interpret metacharacters, so even malicious input in eventText is treated as a literal string argument to openclaw.

Behavior Preservation

The fix maintains identical functionality:
- Same 15-second timeout
- Same UTF-8 encoding
- Same stdio configuration
- Same return value handling with JSON.parse(result.trim())

Only the execution mechanism changes—making it strictly safer with no functional regression.

Key Takeaways

  • Never use execSync with string concatenation in workflow dispatchers: The GatewayWorkflowDispatcherV2.dispatchEvent() method at line 568 was a textbook command injection vector.

  • JSON.stringify is not shell escaping: It produces valid JavaScript/JSON strings, not shell-safe strings. The shell interprets its output differently than JavaScript does.

  • Array-based argument passing eliminates entire vulnerability classes: execFileSync with ['system', 'event', '--mode', 'now', '--json', '--text', eventText] makes injection structurally impossible.

  • Proactive removal of exploit primitives matters: This fix removes a code pattern that, while not independently exploitable in isolation, could be chained with other weaknesses by automated exploit-development tooling.

  • Semgrep's detect-child-process rule catches this pattern: Integrate this rule into your security scanning to prevent similar issues in gateway-workflow-dispatcher-v2.js and other files.

How Orbis AppSec Detected This

Field Details
Source The eventText parameter in GatewayWorkflowDispatcherV2 class methods
Sink execSync() call at line 568 in gateway-workflow-dispatcher-v2.js
Missing control No validation that eventText lacks shell metacharacters; use of string concatenation instead of array-based arguments
CWE CWE-78: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')
Fix Replaced execSync with execFileSync and refactored from string concatenation to array-based argument passing

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 gateway-workflow-dispatcher-v2.js command injection vulnerability demonstrates how a single API choice—execSync versus execFileSync—can mean the difference between secure code and a critical vulnerability. The fix is elegant in its simplicity: by avoiding shell interpretation entirely, we eliminate an entire class of attacks without changing application behavior.

For Node.js developers, this case reinforces a fundamental principle: treat the shell as a dangerous dependency. When you don't need shell features, don't use shell-based APIs. The execFileSync pattern shown here should be your default for executing external commands—it's safer, clearer, and just as capable for the vast majority of use cases.

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #21

Related Articles

high

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

A high-severity command injection vulnerability was discovered in `server.js` where user-controlled file paths were passed directly to shell commands via `exec()`. By migrating from `exec()` to `execFile()` and using argument arrays instead of string concatenation, the fix eliminates the attack surface while preserving the intended trash/delete functionality across macOS, Windows, and Linux.

high

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

A semgrep scan flagged `scripts/postinstall.js` for calling `child_process.execSync` in a way that could become a command injection primitive if the script's execution context ever changed. The fix hardens the script by guarding its side effects behind a `require.main === module` check, introducing the safer `execFileSync` API, and adding automated tests to lock in the safe behavior.

high

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

A critical command injection vulnerability in `scripts/check-links.js` was fixed by replacing `execSync()` with `execFileSync()`, eliminating shell interpretation of user-controlled repository names. This proactive hardening prevents potential remote code execution in the GitHub CLI integration workflow.

critical

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

A critical command injection vulnerability in `scripts/sync-skill.mjs` allowed attackers to execute arbitrary commands through malicious command-line arguments. The fix implements strict whitelist validation on `process.argv` inputs, ensuring only the `--check` flag is accepted before any shell interaction occurs.

high

How command injection happens in JavaScript child_process and how to fix it

A high-severity command injection vulnerability in Claude Code's `prepare-native.js` could have allowed attackers to execute arbitrary shell commands through malicious npm package tarball URLs. The fix adds strict URL scheme validation and proper curl argument termination to neutralize injection vectors.

high

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

The Spotify CLI contained a command injection vulnerability in its browser-opening functionality, where user-controlled URLs were passed directly to `exec()` with shell interpretation enabled. By switching from `exec()` to `execFile()` and properly structuring command arguments, the fix eliminates the attack surface while maintaining cross-platform compatibility.