Prevent Command Injection in Node.js child_process

Using the child_process module in Node.js allows developers to execute system commands, but it introduces severe security risks like command injection if user input is handled improperly. This article outlines the essential best practices for securing your Node.js applications against command injection, focusing on using safe APIs, avoiding shell execution, and validating inputs.

1. Avoid Using exec and execSync

The exec and execSync functions spawn a shell (such as /bin/sh on Unix or cmd.exe on Windows) to execute the passed string. If user input is concatenated directly into this string, attackers can append malicious commands using shell metacharacters like ;, &, or |.

Vulnerable Example:

const { exec } = require('child_process');
// If user_input is "image.png; rm -rf /", the system will delete files
exec(`convert ${user_input} output.png`); 

2. Use execFile or spawn Instead

The safest alternative is to use execFile, execFileSync, spawn, or spawnSync. These functions do not invoke a system shell by default. Instead, they execute the file directly and pass arguments as an array. Since no shell is invoked, shell metacharacters are treated as literal arguments rather than executable commands.

Secure Example:

const { execFile } = require('child_process');
// Arguments are passed safely as an array
execFile('convert', [user_input, 'output.png'], (error, stdout, stderr) => {
  if (error) {
    throw error;
  }
  console.log(stdout);
});

3. Keep shell: false

When using spawn or execFile, you can pass an options object. Ensure you do not set the shell option to true. Setting { shell: true } forces Node.js to run the command inside a shell, which reintroduces the exact command injection vulnerabilities that spawn is designed to prevent.

Vulnerable Example (Avoid this):

// This makes spawn just as vulnerable as exec
spawn('convert', [user_input, 'output.png'], { shell: true }); 

4. Implement Strict Input Validation

Even when using safe APIs like execFile, you should still validate and sanitize all user inputs. Implement a strict allowlist of permitted characters, formats, or values.

// Example of strict regex validation for alphanumeric filenames
const safeInputPattern = /^[a-zA-Z0-9_\-\.]+$/;
if (!safeInputPattern.test(user_input)) {
  throw new Error("Invalid input detected.");
}

5. Run with Least Privilege

If a command injection vulnerability is ever successfully exploited, the damage is limited by the privileges of the Node.js process. Never run your Node.js application as the root or Administrator user. Run the process under a dedicated, low-privilege system user account that only has access to the specific directories and binaries required for operation.