Command injection

Command injection

Description

In Node.js, using {shell: true} or {shell: '/bin/sh'} with child_process.spawn or spawnSync runs an external command through a shell. Special characters in user input may then be interpreted as commands, allowing an attacker to inject unintended operations.

Potential impact

  • Malicious commands that access, delete, or expose server files
  • Exposure, modification, or destruction of sensitive data
  • Service disruption through resource exhaustion or process termination

Remediation

  • Use a fixed executable and pass arguments as an array with {shell: false}.
  • If a shell is required, restrict commands and arguments to a trusted allow-list.
  • Validate external input before using it in commands or arguments. Do not rely on quoting alone.

Examples

Before

javascript
const { spawn } = require("child_process");
const app = require("express")();

app.get("/list", (req, res) => {
  // Request input becomes an argument interpreted by the shell.
  const userInput = req.query.path;
  const p = spawn("ls", ["-lh", userInput], { shell: true });
});

After

javascript
const { spawn } = require("child_process");
const app = require("express")();

app.get("/list", (req, res) => {
  const userInput = req.query.path;
  if (typeof userInput !== "string" || !/^[a-zA-Z0-9_-]+$/.test(userInput)) {
    return res.sendStatus(400);
  }
  // Pass validated arguments to a fixed executable without a shell.
  const p = spawn("ls", ["-lh", "--", userInput], { shell: false });
});

Explanation:

  • Before: With shell: true, special characters in user input may be interpreted by the shell as commands.
  • After: shell: false (the default) avoids shell interpretation, and the input format is restricted. -- stops ls from treating the input as an option. The executable location and access to the directories being listed still need separate controls.

References