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.

Prevention & Best Practices

1. Prefer execFile/execFileSync Over exec/execSync

Use Case Recommended API
Execute command with arguments execFile / execFileSync
Need shell features (wildcards, pipes) exec with extreme caution + validation
Complex pipelines Consider spawn with manual pipe handling

2. If You Must Use Shell Execution

When shell features are unavoidable:
- Use spawn with shell: true and the args option (Node.js 5.7+)
- Apply strict allowlist validation to all inputs
- Consider shell-quote or similar libraries for escaping

3. Static Analysis Integration

The Semgrep rule javascript.lang.security.detect-child-process.detect-child-process that flagged this issue should be part of your CI pipeline:

# .github/workflows/security.yml
- uses: returntocorp/semgrep-action@v1
  with:
    config: >-
      p/security-audit
      p/owasp-top-ten
      p/cwe-top-25

4. Defense in Depth

Even with safe APIs, validate inputs against expected patterns:

// Additional hardening for eventText
const VALID_EVENT_TEXT = /^[\w\s\-.,:;!?()[\]{}]{1,10000}$/;
if (!VALID_EVENT_TEXT.test(eventText)) {
  throw new ValidationError('Invalid event text format');
}

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.

References

Frequently Asked Questions

What is command injection?

Command injection occurs when an attacker can execute arbitrary system commands on a host by injecting malicious input into a command string that gets passed to a shell interpreter.

How do you prevent command injection in Node.js?

Use execFileSync or spawn with array-based arguments instead of exec/execSync with string concatenation. Avoid shell interpretation entirely when possible.

What CWE is command injection?

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

Is input sanitization enough to prevent command injection?

Input sanitization alone is fragile and error-prone. The safest approach is to avoid shell interpretation entirely by using APIs that pass arguments as arrays rather than strings.

Can static analysis detect command injection?

Yes, tools like Semgrep can detect dangerous child_process patterns. The semgrep rule `javascript.lang.security.detect-child-process.detect-child-process` specifically flags risky exec/execSync usage.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #21

Related Articles

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.

critical

How command injection happens in Kotlin/Android and how to fix it

V2rayNG's RootShell.kt built root shell commands by concatenating an unescaped file path directly into a string passed to `su -c`, creating a critical command injection risk (CWE-78). The fix restricts the `exec()` API to internal use only and single-quote-escapes the file path before it ever reaches the root shell.

high

How shell command injection happens in Ruby and how to fix it

A critical command injection vulnerability was discovered in Fastlane's deliver module where `system("open '#{html_path}'")` allowed shell metacharacters in file paths to execute arbitrary commands. The fix replaces vulnerable string interpolation with array-based argument passing, eliminating the shell entirely.

critical

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

CVE-2026-9277 is a critical command injection vulnerability in shell-quote versions prior to 1.8.4 that allows attackers to execute arbitrary code by injecting unescaped line terminators into shell commands. This vulnerability affects any Node.js application that uses the vulnerable shell-quote package to construct shell commands from untrusted input. The fix upgrades shell-quote to version 1.8.4, which properly escapes line terminators and neutralizes the injection vector.

high

How command injection happens in Ruby and how to fix it

A Fastlane helper used a Ruby backtick subshell to clone a plugin's git repository, interpolating `self.homepage` directly into a shell command string. Even with `shellescape` applied, the pattern was flagged as a dangerous subshell that could be chained into a command injection primitive; the fix replaces it with `system()` using an argument array, eliminating shell interpretation entirely.

critical

How command injection happens in JavaScript dependency trees and how to fix it

A critical command injection vulnerability in websocket-driver 0.7.4 allowed attackers to execute arbitrary shell commands through unescaped line terminators in WebSocket protocol handling. The automated fix upgrades to version 0.7.5 and adds an explicit override in package.json to prevent dependency resolution from reverting to the vulnerable version.