FlawOpen

flawopen.com/Command Injection/Node child_process.exec

Is Node's child_process.exec() safe?

Reference page
Plain English

Passing a command string to exec() is like giving a sentence to a typesetter and letting punctuation decide where the instructions split. If untrusted text contains shell syntax, the shell can read it as control instructions. execFile() sends a fixed program and separate arguments, so there is no shell parsing by default.

VULNERABLE
const { exec } = require('node:child_process');

function inspectFile(filename) {
  return exec(`file ${filename}`);
}
FIXED
const { execFile } = require('node:child_process');

const allowed = new Map([
  ['report-1', '/srv/reports/1.pdf'],
  ['report-2', '/srv/reports/2.pdf']
]);

function inspectReport(reportId) {
  const path = allowed.get(reportId);
  if (!path) throw new Error('Unknown report');
  return execFile('/usr/bin/file', ['--', path], {
    shell: false,
    timeout: 3000,
    maxBuffer: 64 * 1024
  });
}

Why execFile() reduces command-injection risk

exec() starts a shell and passes it one command string. When application code interpolates attacker-controlled text into that string, the shell parses metacharacters as syntax; this is the command-injection path described by CWE-78. execFile() launches a specific executable with an argument array and does not spawn a shell by default, so shell separators in an argument are not reparsed by a shell. The example also maps a caller-supplied ID to a fixed path. This does not make every target program safe: validate the arguments the program accepts, use an option terminator such as -- when supported, and never enable shell: true for untrusted values.

How to find unsafe exec() calls

Search for exec() and execSync(), then trace each command string back to request parameters, database fields, filenames, and environment variables. Record the executable and every argument boundary. Prefer a direct library API; otherwise use execFile() with a fixed executable, validated arguments, bounded runtime, and limited output.

grep -RInE 'exec(Sync)?[[:space:]]*\(' --include='*.js' --include='*.cjs' --include='*.mjs' --include='*.ts' .

Synchronous variants

What about execSync?

execSync() uses the same shell-based command string and has the same injection risk, while also blocking the event loop. execFileSync() avoids shell parsing by default, but its executable and arguments still need validation.

References

See also OS Command Injection (CWE-78): How It Works and How to Prevent It in Every Language — flawopen.com exec() vs execFile() in Node.js: What's the Difference? Java Runtime.exec() and Command Injection: What Changes?