flawopen.com/command-injection/Javascript
OS Command Injection in JavaScript (child_process, Node.js)
child_process.exec() with a template literal, execFile() with shell: true, or a user value that starts with a dash lets a request run extra commands on your Node.js server (CWE-78). The safe execFile/spawn call, the Windows batch-file trap behind CVE-2024-27980, and the lint rules that catch all three.
Think of a rail dispatcher receiving a destination. child_process.exec() sends a whole sentence to a shell, which treats punctuation such as ;, | and && as new instructions. execFile() and spawn() pass an argument array directly, so punctuation stays inside that one argument. Setting shell: true joins it back into shell input. Windows .bat and .cmd files are a special boundary too: cmd.exe reparses arguments, the issue tracked as CVE-2024-27980. Fix the executable, validate each field, keep shell disabled, and avoid batch files for user-controlled arguments.
Core Concepts & Subsystem Terms
child_process.exec()- Runs its whole first argument through
/bin/sh -c(orcmd.exeon Windows). A template literal such as `ping -c 1 ${host}puts user text where the shell reads;,|,&&and$( )`. execFile() / spawn() with an argument arrayexecFile(file, args)andspawn(command, args)start the program directly, without a shell. Each array element is passed as exactly one argument, so shell metacharacters inside it are ordinary text.The shell: true option- Passing
{ shell: true }toexecFileorspawnjoins the arguments into one command line and hands it to a shell again, bringing back the injection the array was meant to prevent. Leave it off. Windows .bat / .cmd files (CVE-2024-27980)- Windows cannot execute
.bator.cmdfiles directly; it startscmd.exe, which parses the arguments with its own rules. Before the April 2024 releases,spawn('run.bat', [userInput])was injectable withoutshell: true. Patched Node.js throwsEINVALinstead. Argument injection and Git option boundaries- Separate argv entries prevent shell parsing, but the target program still parses its own options. For an untrusted Git revision, use
git rev-parse --verify --end-of-options "$revision": this ends option parsing.--has command-specific operand or path semantics; ingit log, it separates revisions from paths. These markers are not interchangeable. Validate that the value is a revision before use.
Framework mappings
- CWE reference for the weakness described in this guide: CWE-78
- OWASP Top 10:2025 — A05:2025 Injection
Step-by-Step Attack Flow
Request value reaches the helper
An Express route passes req.query.host to ping() or a directory name from the request body to archive().
A template literal builds a command line
` ping -c 1 ${host} is evaluated first. With the benign test value 127.0.0.1; id, the command line becomes ping -c 1 127.0.0.1; id`.
The shell splits it into two commands
exec hands the string to /bin/sh -c, which runs ping and then id as the Node.js process user. With shell: true, execFile does the same; without a shell, an untrusted argument such as Git’s --output=<file> can still be parsed as an option.
The attacker runs code on the server
The injected command inherits the process environment: API keys in process.env, cloud credentials, the network position of the server and write access to its files.
Source Code: Flaw vs. Secure Implementation
// tools.js: each helper below lets request text reach a command line
import { exec, execFile } from 'node:child_process';
import { promisify } from 'node:util';
const execAsync = promisify(exec);
const execFileAsync = promisify(execFile);
// exec runs the whole template literal through /bin/sh
export async function ping(host) {
const { stdout } = await execAsync(`ping -c 1 ${host}`);
return stdout;
}
// shell: true joins the array back into a shell command line
export async function archive(dir) {
const { stdout } = await execFileAsync('tar', ['-czf', 'backup.tgz', dir], { shell: true });
return stdout;
}
// no shell, but a ref starting with - is read as a Git option
export async function commitLog(ref) {
const { stdout } = await execFileAsync('git', ['log', ref]);
return stdout;
}
// tools.js: argument arrays, strict patterns and end-of-options markers
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
const execFileAsync = promisify(execFile);
const HOST_RE = /^[A-Za-z0-9][A-Za-z0-9.-]{0,252}$/;
const DIR_RE = /^[A-Za-z0-9_-]{1,64}$/;
// execFile starts ping directly; host is one argument
export async function ping(host) {
if (!HOST_RE.test(host)) throw new Error('invalid host name');
const { stdout } = await execFileAsync('ping', ['-c', '1', host], { timeout: 5000 });
return stdout;
}
// no shell option, and -- ends tar's option parsing
export async function archive(dir) {
if (!DIR_RE.test(dir)) throw new Error('invalid directory name');
const { stdout } = await execFileAsync('tar', ['-czf', 'backup.tgz', '--', dir], { timeout: 30000 });
return stdout;
}
// --end-of-options makes Git treat ref as a revision
export async function commitLog(ref) {
const { stdout } = await execFileAsync('git', ['log', '--end-of-options', ref, '--'], { timeout: 5000 });
return stdout;
}
Engineering & System Hardening Checklist
- ✓Enable
security/detect-child-processfrom eslint-plugin-security (or a Semgrep rule) in CI and banexec/execSyncwith non-literal arguments. - ✓Use
execFileorspawnwith an argument array, nevershell: true, and set atimeouton every call. - ✓Reject option-like values by default. For an untrusted Git revision, call
git rev-parse --verify --end-of-options "$revision"; use--only with the operand or path meaning documented by that exact command. - ✓Never spawn
.bator.cmdfiles with user-controlled arguments on Windows, and run a Node.js release that includes theCVE-2024-27980fix. - ✓Run the service as an unprivileged user without secrets in its environment beyond what it needs.
Sources
- CWE-78: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')
- Node.js docs — child_process: Spawning .bat and .cmd files on Windows
- Node.js docs — child_process.execFile
- Node.js — April 2024 security releases (CVE-2024-27980)
- eslint-plugin-security — detect-child-process
- OWASP OS Command Injection Defense Cheat Sheet
- POSIX Utility Syntax Guidelines
- Git rev-parse — --end-of-options
- Git — Command-Line Interface Conventions
- Git — git log